Skip to content

Instantly share code, notes, and snippets.

@PirosB3
Last active March 20, 2024 01:12
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save PirosB3/8141b51fbb307bca265866ef1cef564f to your computer and use it in GitHub Desktop.
Save PirosB3/8141b51fbb307bca265866ef1cef564f to your computer and use it in GitHub Desktop.
Python V4 RFQ Guide
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Wintermute](https://miro.medium.com/max/4206/1*cyHT1jvRUvdRrxVIi-V2Rw.png)\n",
"\n",
"# 0x V4 RFQ upgrade guide\n",
"\n",
"Learn how to create, hash, sign, fill, get state, and cancel 0x V4 RFQ orders through this Gist!"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import eth_abi\n",
"import web3\n",
"from eth_utils import keccak, remove_0x_prefix, to_bytes\n",
"\n",
"import hashlib\n",
"from bitcoin import ecdsa_raw_sign\n",
"from typing import NamedTuple, Tuple\n",
"\n",
"import time\n",
"import os"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup process\n",
"\n",
"Before being able to perform any trades, we create 2 random private keys (previously funded with Ropsten ETH) and we mint some dummy tokens."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"# Create 2 private keys. These keys have been funded with some Ropsten ETH. For these scripts to work, the accounts below need to have some Ropsten ETH\n",
"private_key_maker = web3.Web3.toBytes(hexstr='0xa621b180451db305c18e0def0875651767d5551781ce33d8e6aa0643b876b1ec')\n",
"private_key_maker_address = '0x66d9357650beF62E9Ca5b7E250091Cf50D06413e'\n",
"private_key_taker = web3.Web3.toBytes(hexstr='0xa621b180451db305c18e0def0875651767d5551781ce33d8e6aa0643b876b186')\n",
"private_key_taker_address='0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1'\n",
"\n",
"# Fetch the RPC URL and the Exchange Proxy address\n",
"ROPSTEN_URL = os.environ['ROPSTEN_ETH_URL']\n",
"ROPSTEN_EP = '0xdef1c0ded9bec7f1a1670819833240f027b25eff'\n",
"\n",
"# I've deployed 2 Dummy ERC20 tokens for this demo, we can use these tokens for trading.\n",
"TOKEN_A = web3.Web3.toChecksumAddress('0xF84830B73b2ED3C7267E7638f500110eA47FDf30')\n",
"TOKEN_B = web3.Web3.toChecksumAddress('0x374a16f5e686c09b0cc9e8bc3466b3b645c74aa7')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create Web3 connection"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Latest block is: 10322367\n"
]
}
],
"source": [
"provider = web3.HTTPProvider(ROPSTEN_URL)\n",
"client = web3.Web3(provider)\n",
"print(f\"Latest block is: {client.eth.getBlock('latest')['number']}\")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"client.eth.chain_id"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Preparation: Mint tokens and set allowances\n",
"\n",
"In order to perform the swap, each address needs to set an allow the Exchange Proxy to trade both `TOKEN_A` and `TOKEN_B`. These tokens conveniently have a `mint()` function that allows these addresses to mint a specified number of tokens, this will allow both accounts to have some tokens to trade"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Mint tokens"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def mint_tokens(private_key, private_key_address, token_address):\n",
" \"\"\"\n",
" Mints some tokens of `token_address` for a particular EOA, given that EOA private key.\n",
" The token in question should expose a `mint()` function.\n",
" \"\"\"\n",
" function_signature = web3.Web3.keccak(text='mint(uint256)').hex()[:10]\n",
" encoded_args = eth_abi.encode_abi(['uint256'], [int(1000 * 1e18)])\n",
" calldata = function_signature + encoded_args.hex()\n",
" tx = {\n",
" \"from\": private_key_address,\n",
" \"to\": client.toChecksumAddress(token_address),\n",
" \"data\": calldata,\n",
" \"gasPrice\": int(10 * 1e9),\n",
" \"gas\": 500_000,\n",
" \"nonce\": client.eth.getTransactionCount(private_key_address),\n",
" }\n",
" signed = client.eth.account.sign_transaction(tx, private_key)\n",
" tx_receipt = client.eth.sendRawTransaction(signed.rawTransaction)\n",
" client.eth.waitForTransactionReceipt(tx_receipt)\n",
" print(f\"Transaction {tx_receipt.hex()} was successfully mined.\")\n",
" return tx_receipt"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Set allowance"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def set_allowance(private_key, private_key_address, token_address, target=ROPSTEN_EP):\n",
" \"\"\"\n",
" Allows the target (Exchange Proxy) to spend `token_address` tokens of EOA `private_key_address`\n",
" \"\"\"\n",
" function_signature = web3.Web3.keccak(text='approve(address,uint256)').hex()[:10]\n",
" encoded_args = eth_abi.encode_abi(['address', 'uint256'], [target, int(1000 * 1e18)])\n",
" calldata = function_signature + encoded_args.hex()\n",
" tx = {\n",
" \"from\": private_key_address,\n",
" \"to\": client.toChecksumAddress(token_address),\n",
" \"data\": calldata,\n",
" \"gasPrice\": int(10 * 1e9),\n",
" \"gas\": 500_000,\n",
" \"nonce\": client.eth.getTransactionCount(private_key_address),\n",
" }\n",
" signed = client.eth.account.sign_transaction(tx, private_key)\n",
" tx_receipt = client.eth.sendRawTransaction(signed.rawTransaction)\n",
" client.eth.waitForTransactionReceipt(tx_receipt)\n",
" print(f\"Transaction {tx_receipt.hex()} was successfully mined.\")\n",
" return tx_receipt"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Transaction 0xbaca7a5e70b2febe47c8cdb80e222aecdbc6d6e48d4441b674f00dcb2ac48243 was successfully mined.\n",
"Transaction 0x2b4cf330e21d65662beb2299792441fa714ffe71f0d54cb01640dec58d67a60a was successfully mined.\n",
"Transaction 0xd36a55e7b4d5d21ea4a02e5d10ece40683ac60a188304571854a84ba8a64625e was successfully mined.\n",
"Transaction 0xbd4c8a8196c9f190263438edf8f4801ea020335728422d631be85d186c82457f was successfully mined.\n"
]
},
{
"data": {
"text/plain": [
"HexBytes('0xbd4c8a8196c9f190263438edf8f4801ea020335728422d631be85d186c82457f')"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"mint_tokens(private_key_taker, private_key_taker_address, TOKEN_A)\n",
"mint_tokens(private_key_taker, private_key_taker_address, TOKEN_B)\n",
"mint_tokens(private_key_maker, private_key_maker_address, TOKEN_A)\n",
"mint_tokens(private_key_maker, private_key_maker_address, TOKEN_B)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Transaction 0x8b38968bcd46a713e19ebb2620fedc61eb1dfca0e2baa6e99c770c65160f5395 was successfully mined.\n",
"Transaction 0x617b792fd6921c4d742b3c42090a5c465ed3fc6d27e13faeb7c37f01e268dc13 was successfully mined.\n",
"Transaction 0x88a70218fdee20d3f4593e3e7e2836c8d71c9a5adb5c7ace013bacb7a3a3b65e was successfully mined.\n",
"Transaction 0xa7eaad41e4fc08305d28ddc13e7474d8a8da446689abc503e95ccb7b88d4561a was successfully mined.\n"
]
},
{
"data": {
"text/plain": [
"HexBytes('0xa7eaad41e4fc08305d28ddc13e7474d8a8da446689abc503e95ccb7b88d4561a')"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"set_allowance(private_key_taker, private_key_taker_address, TOKEN_A)\n",
"set_allowance(private_key_taker, private_key_taker_address, TOKEN_B)\n",
"set_allowance(private_key_maker, private_key_maker_address, TOKEN_A)\n",
"set_allowance(private_key_maker, private_key_maker_address, TOKEN_B)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# V4 Specific operations for RFQ"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create an order\n",
"\n",
"We go ahead and create a new RFQ order, which is selling 1000 of `TOKEN_A` towards 1000 of `TOKEN_B`. The `private_key_taker_address` is set as the taker as well as the `txOrigin`."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"RfqOrder(makerToken='0xF84830B73b2ED3C7267E7638f500110eA47FDf30', takerToken='0x374a16F5E686c09B0CC9e8bC3466b3B645C74aa7', makerAmount=55000000000000000000, takerAmount=1000000000000000000000, maker='0x66d9357650beF62E9Ca5b7E250091Cf50D06413e', taker='0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1', txOrigin='0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1', pool=23, expiry=1653688296, salt=1622157380)"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# We define here the RFQ order spec.\n",
"\n",
"rfq_order_field_types = [\n",
" \"address\",\n",
" \"address\",\n",
" \"uint128\",\n",
" \"uint128\",\n",
" \"address\",\n",
" \"address\",\n",
" \"address\",\n",
" \"bytes32\",\n",
" \"uint64\",\n",
" \"uint256\",\n",
"]\n",
"\n",
"\n",
"class RfqOrder(NamedTuple):\n",
" makerToken: str\n",
" takerToken: str\n",
" makerAmount: int\n",
" takerAmount: int\n",
" maker: str\n",
" taker: str\n",
" txOrigin: str\n",
" pool: int\n",
" expiry: int\n",
" salt: int\n",
"\n",
"\n",
"order = RfqOrder(\n",
" maker=private_key_maker_address,\n",
" taker=private_key_taker_address,\n",
" makerToken=TOKEN_A,\n",
" takerToken=TOKEN_B,\n",
" salt=int(time.time()),\n",
" expiry=1653688296, # Some time in 2022\n",
" txOrigin=private_key_taker_address,\n",
" pool=23, # Pool here is set, however it does not have any bearing on the trade\n",
" makerAmount=int(55 * 1e18),\n",
" takerAmount=int(1000 * 1e18),\n",
")\n",
"order"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Hash an order\n",
"\n",
"We show how to hash an order locally."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"# --- Start internal functions ---\n",
"\n",
"eip191_header = b\"\\x19\\x01\"\n",
"\n",
"eip712_domain_separator_schema_hash = client.keccak(\n",
" b\"EIP712Domain(\"\n",
" + b\"string name,\"\n",
" + b\"string version,\"\n",
" + b\"uint256 chainId,\"\n",
" + b\"address verifyingContract\"\n",
" + b\")\"\n",
")\n",
"\n",
"eip712_order_schema_hash = client.keccak(\n",
" b\"RfqOrder(\"\n",
" + b\"address makerToken,\"\n",
" + b\"address takerToken,\"\n",
" + b\"uint128 makerAmount,\"\n",
" + b\"uint128 takerAmount,\"\n",
" + b\"address maker,\"\n",
" + b\"address taker,\"\n",
" + b\"address txOrigin,\"\n",
" + b\"bytes32 pool,\"\n",
" + b\"uint64 expiry,\"\n",
" + b\"uint256 salt\"\n",
" + b\")\"\n",
")\n",
"\n",
"def pad_20_bytes_to_32(twenty_bytes: bytes):\n",
" return bytes(12) + twenty_bytes\n",
"\n",
"def int_to_32_big_endian_bytes(i: int):\n",
" return i.to_bytes(32, byteorder=\"big\")\n",
"\n",
"\n",
"def make_eip712_domain_struct_header_hash(chain_id: int, verifying_contract: str) -> str:\n",
" return keccak(\n",
" eip712_domain_separator_schema_hash\n",
" + keccak(b\"ZeroEx\")\n",
" + keccak(b\"1.0.0\")\n",
" + int_to_32_big_endian_bytes(int(chain_id))\n",
" + pad_20_bytes_to_32(to_bytes(hexstr=verifying_contract))\n",
" )\n",
"\n",
"# --- End internal functions ---\n",
"\n",
"\n",
"def get_order_hash_0xv4(order: RfqOrder, chain_id: int, exchange_proxy_address: str):\n",
" \"\"\"\n",
" Returns an order hash for a given RFQ order. The order hash will not contain the `0x` prefix.\n",
" \n",
" :params order: a RFQ order\n",
" :params chain_id: the Chain ID. Ropsten is 3, while Mainnet is 1\n",
" :params exchange_proxy_address: the address of the Exchange Proxy. This address must be checksummed.\n",
" \"\"\"\n",
"\n",
" eip712_order_struct_hash = client.keccak(\n",
" eip712_order_schema_hash\n",
" + pad_20_bytes_to_32(to_bytes(hexstr=order.makerToken))\n",
" + pad_20_bytes_to_32(to_bytes(hexstr=order.takerToken))\n",
" + int_to_32_big_endian_bytes(int(order.makerAmount))\n",
" + int_to_32_big_endian_bytes(int(order.takerAmount))\n",
" + pad_20_bytes_to_32(to_bytes(hexstr=order.maker))\n",
" + pad_20_bytes_to_32(to_bytes(hexstr=order.taker))\n",
" + pad_20_bytes_to_32(to_bytes(hexstr=order.txOrigin))\n",
" + int_to_32_big_endian_bytes(int(order.pool))\n",
" + int_to_32_big_endian_bytes(int(order.expiry))\n",
" + int_to_32_big_endian_bytes(int(order.salt))\n",
" )\n",
"\n",
" eip712_domain_struct_hash = make_eip712_domain_struct_header_hash(\n",
" chain_id=chain_id,\n",
" verifying_contract=exchange_proxy_address\n",
" )\n",
" \n",
" print(f\"Domain hash: {eip712_domain_struct_hash.hex()}\")\n",
" print(f\"Order schema hash: {eip712_order_schema_hash.hex()}\")\n",
"\n",
" return keccak(\n",
" eip191_header\n",
" + eip712_domain_struct_hash\n",
" + eip712_order_struct_hash\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Domain hash: 5855fcb510668b0dc493b864e7420f55baa055dc0348feb0d4d2c261c384098a\n",
"Order schema hash: 0xe593d3fdfa8b60e5e17a1b2204662ecbe15c23f2084b9ad5bae40359540a7da9\n"
]
},
{
"data": {
"text/plain": [
"'85b4524fc99314a164801c94ebaed723cfe7386be5e877c7c5c24aa4f9db671a'"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"off_chain_order_hash = get_order_hash_0xv4(order, 3, ROPSTEN_EP)\n",
"off_chain_order_hash.hex()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Get RFQ Order Info\n",
"\n",
"We suggest to call `getRfqOrderInfo()` to get the current on-chain state of orders. You can read more about this [here](https://0xprotocol.readthedocs.io/en/latest/basics/functions.html#getrfqorderinfo)."
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hash hex: 85b4524fc99314a164801c94ebaed723cfe7386be5e877c7c5c24aa4f9db671a\n",
"Fillable status: 1\n",
"Amount filled: 0.0\n"
]
}
],
"source": [
"def get_order_info(order: RfqOrder) -> Tuple[bytes, int, int]:\n",
" function_signature = web3.Web3.keccak(text=f'getRfqOrderInfo(({\",\".join(rfq_order_field_types)}))').hex()[:10]\n",
" encoded_args = eth_abi.encode_abi(rfq_order_field_types, [\n",
" order.makerToken,\n",
" order.takerToken,\n",
" order.makerAmount,\n",
" order.takerAmount,\n",
" order.maker,\n",
" order.taker,\n",
" order.txOrigin,\n",
" int_to_32_big_endian_bytes(order.pool), # Pool is represented as an unsigned integer, however it is encoded as a `bytes32`\n",
" order.expiry,\n",
" order.salt\n",
" ])\n",
" calldata = function_signature + encoded_args.hex()\n",
" tx = {\n",
" \"to\": client.toChecksumAddress(ROPSTEN_EP),\n",
" \"data\": calldata,\n",
" }\n",
"\n",
" # Perform an `eth_call` to read the transaction data result. The response structure can be found here:\n",
" # https://0xprotocol.readthedocs.io/en/latest/basics/functions.htbml#getrfqorderinfo\n",
" response = client.eth.call(tx)\n",
" on_chain_order_hash, fillable_status, amount_filled = eth_abi.decode_abi(['bytes32', 'uint8', 'uint128'], bytes.fromhex(response.hex()[2:]))\n",
"\n",
" print(f\"Hash hex: {on_chain_order_hash.hex()}\")\n",
" print(f\"Fillable status: {fillable_status}\")\n",
" print(f\"Amount filled: {amount_filled / 1e18}\")\n",
" return on_chain_order_hash, fillable_status, amount_filled\n",
"\n",
"\n",
"on_chain_order_hash, fillable_status, amount_filled = get_order_info(order)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"# Safety check, ensure that the order hash generated off-chain is the same one that is generated on-chain\n",
"\n",
"assert on_chain_order_hash.hex() == off_chain_order_hash.hex()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Signing the Order Hash\n",
"\n",
"For some reason, I was not able to use your `SignHash` function but I am concerned that it's beacause I have an incorrect version of your library. However, I was able to provide you with a drop-in alternative that does the exact same thing that your code does and takes in the same parameters."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"def SignHashNew(hash_bytesArray, privateKey):\n",
" from eth_account.messages import encode_defunct\n",
" message = encode_defunct(hash_bytesArray)\n",
" signed_message = client.eth.account.sign_message(message, private_key=privateKey)\n",
" return (\n",
" signed_message.v,\n",
" '0x' + signed_message.r.to_bytes(length=32, byteorder='big').hex(),\n",
" '0x' + signed_message.s.to_bytes(length=32, byteorder='big').hex()\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(27,\n",
" b'\\x0b\\x0c\\x93FV\\xa8\\xf2\\x95c\\xb7\\x96!y`*\\xa7\\xe7\\x18N\\xa1\\xab\\xb8\\x96m\\xcd\\xa8\\x93\\xbd\\x0f\\xc02\\x98',\n",
" b'lq\\x81\\xb5\\xe3\\xf2\\xd2\\x9c\\xd3\\x0e\\x8c\\xec\\x13\\xb7\\xe9I\\xf5Y\\xecV\\x7f`\\x05o\\xe9!{\\xaa\\xa99$\\x0c')"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"v, r, s = SignHashNew(off_chain_order_hash, private_key_maker)\n",
"r = bytes.fromhex(r[2:])\n",
"s = bytes.fromhex(s[2:])\n",
"\n",
"v, r, s"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Filling a RFQ order through the VIP route\n",
"\n",
"As long as you don't need aggregation, you can fill a V4 RFQ order for **real cheap**. The signature field passed into `fillRfqOrder` is now a struct instead of a bytes payload. I've gone ahead and shown you how you can fill this order that we created.\n",
"\n",
"\n",
"### Order schema\n",
"\n",
"```\n",
"struct {\n",
" uint8 signatureType; // Either 2 or 3\n",
" uint8 v; // Signature data.\n",
" bytes32 r; // Signature data.\n",
" bytes32 s; // Signature data.\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"rfq_order_field_tuple = f'({\",\".join(rfq_order_field_types)})'\n",
"signature_field_tuple = '(uint8,uint8,bytes32,bytes32)'"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'fillRfqOrder((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)'"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"function_definition = f'fillRfqOrder({rfq_order_field_tuple},{signature_field_tuple},uint128)'\n",
"function_signature = web3.Web3.keccak(text=function_definition).hex()[:10]\n",
"\n",
"function_definition"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [],
"source": [
"def fill_rfq_order(taker_private_key, taker_address, order, v, r, s, fill_amount):\n",
" encodes = [\n",
" rfq_order_field_tuple,\n",
" signature_field_tuple,\n",
" 'uint128'\n",
" ]\n",
" encoded_args = eth_abi.encode_abi(encodes, [[\n",
" order.makerToken,\n",
" order.takerToken,\n",
" order.makerAmount,\n",
" order.takerAmount,\n",
" order.maker,\n",
" order.taker,\n",
" order.txOrigin,\n",
" int_to_32_big_endian_bytes(order.pool),\n",
" order.expiry,\n",
" order.salt\n",
" ], [3, v, r, s], fill_amount])\n",
" calldata = function_signature + encoded_args.hex()\n",
"\n",
"\n",
" tx = {\n",
" \"from\": taker_address,\n",
" \"to\": client.toChecksumAddress(ROPSTEN_EP),\n",
" \"data\": calldata,\n",
" \"gasPrice\": int(10 * 1e9),\n",
" \"gas\": 500_000,\n",
" \"nonce\": client.eth.getTransactionCount(taker_address),\n",
" }\n",
" signed = client.eth.account.sign_transaction(tx, taker_private_key)\n",
" fill_tx = client.eth.sendRawTransaction(signed.rawTransaction)\n",
" client.eth.waitForTransactionReceipt(fill_tx)\n",
" print(f\"Fill transaction succeded: {fill_tx.hex()}\")\n",
" return fill_tx"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Fill transaction succeded: 0xa77b363cd634706fc49d790fe377902c155f056a963f90fcbc6e64b9666c4409\n"
]
}
],
"source": [
"fill_tx = fill_rfq_order(private_key_taker, private_key_taker_address, order, v, r, s, int(13.5 * 1e18))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Search for events\n",
"\n",
"Events have changed in V4, so we show you how easy it is to check events on the exchange for `RfqOrderFilled` events."
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[AttributeDict({'address': '0xDef1C0ded9bec7F1a1670819833240f027b25EfF', 'topics': [HexBytes('0x829fa99d94dc4636925b38632e625736a614c154d55006b7ab6bea979c210c32')], 'data': '0x85b4524fc99314a164801c94ebaed723cfe7386be5e877c7c5c24aa4f9db671a00000000000000000000000066d9357650bef62e9ca5b7e250091cf50d06413e00000000000000000000000006652bdd5a8eb3d206caedd6b95b61f820abb9b1000000000000000000000000f84830b73b2ed3c7267e7638f500110ea47fdf30000000000000000000000000374a16f5e686c09b0cc9e8bc3466b3b645c74aa7000000000000000000000000000000000000000000000000bb59a27953c600000000000000000000000000000000000000000000000000000a4de3d0e9ba40000000000000000000000000000000000000000000000000000000000000000017', 'blockNumber': 10322379, 'transactionHash': HexBytes('0xa77b363cd634706fc49d790fe377902c155f056a963f90fcbc6e64b9666c4409'), 'transactionIndex': 0, 'blockHash': HexBytes('0x9c796f65ed2285ab22a6ce25ea25d0c871e5b33476957c7025187005639ad979'), 'logIndex': 2, 'removed': False})]\n"
]
}
],
"source": [
"transaction_block = client.eth.getTransaction(fill_tx.hex())['blockNumber']\n",
"logs = client.eth.getLogs({'fromBlock': transaction_block, 'toBlock': transaction_block, 'address': client.toChecksumAddress(ROPSTEN_EP)})\n",
"print(logs)"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Order hash: 85b4524fc99314a164801c94ebaed723cfe7386be5e877c7c5c24aa4f9db671a\n",
"Pool ID: 23\n",
"Taker performed a fill of 13.5 0x374a16f5e686c09b0cc9e8bc3466b3b645c74aa7 for 0.7425 0xf84830b73b2ed3c7267e7638f500110ea47fdf30\n"
]
}
],
"source": [
"fill_event = client.keccak(\n",
" text='RfqOrderFilled(bytes32,address,address,address,address,uint128,uint128,bytes32)'\n",
").hex()\n",
"\n",
"for log in logs:\n",
" topic = log['topics'][0]\n",
" if topic.hex() == fill_event:\n",
" orderHash, maker, taker, makerToken, takerToken, takerTokenFilledAmount, makerTokenFilledAmount, pool = eth_abi.decode_abi([\n",
" 'bytes32',\n",
" 'address',\n",
" 'address',\n",
" 'address',\n",
" 'address',\n",
" 'uint128',\n",
" 'uint128',\n",
" 'bytes32',\n",
" ], bytes.fromhex(log['data'][2:]))\n",
" print(f\"Order hash: {orderHash.hex()}\")\n",
" \n",
" # Ensure fill has the right maker and taker\n",
" assert maker.lower() == private_key_maker_address.lower()\n",
" assert taker.lower() == private_key_taker_address.lower()\n",
" print(f\"Pool ID: {int.from_bytes(pool, byteorder='big')}\")\n",
" print(f\"Taker performed a fill of {takerTokenFilledAmount / 1e18} {takerToken} for {makerTokenFilledAmount / 1e18} {makerToken}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Cancel a pair\n",
"\n",
"Another change in V4 is how `cancelOrderUpto` operations are performed. You can now cancel one side of the orderbook instead of the entire market. However, below we will show you how you can cancel the entire market."
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"function_signature = web3.Web3.keccak(text='batchCancelPairRfqOrders(address[],address[],uint256[])').hex()[:10]\n",
"encoded_args = eth_abi.encode_abi(['address[]','address[]','uint256[]'], [\n",
" [TOKEN_A, TOKEN_B],\n",
" [TOKEN_B, TOKEN_A],\n",
" [int(time.time()), int(time.time())],\n",
"])"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Cancelled market for 0xF84830B73b2ED3C7267E7638f500110eA47FDf30<>0x374a16F5E686c09B0CC9e8bC3466b3B645C74aa7: 0x0eaaa50be019ad72aaf46a1e3f55de131ec42416383c9ae8a69312f648e951c6\n"
]
}
],
"source": [
"calldata = function_signature + encoded_args.hex()\n",
"tx = {\n",
" \"from\": private_key_maker_address,\n",
" \"to\": client.toChecksumAddress(ROPSTEN_EP),\n",
" \"data\": calldata,\n",
" \"gasPrice\": int(10 * 1e9),\n",
" \"gas\": 500_000,\n",
" \"nonce\": client.eth.getTransactionCount(private_key_maker_address),\n",
"}\n",
"\n",
"signed = client.eth.account.sign_transaction(tx, private_key_maker)\n",
"cancel_tx = client.eth.sendRawTransaction(signed.rawTransaction)\n",
"client.eth.waitForTransactionReceipt(cancel_tx)\n",
"print(f\"Cancelled market for {TOKEN_A}<>{TOKEN_B}: {cancel_tx.hex()}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Serialize to JSON\n",
"\n",
"Whenever 0x API requests a RFQ quote, you will need to serialize the `RfqOrder` structure and it's signature to JSON. Below is a guide and some helper functions on how to do that."
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\n",
" \"makerToken\": \"0xF84830B73b2ED3C7267E7638f500110eA47FDf30\",\n",
" \"takerToken\": \"0x374a16F5E686c09B0CC9e8bC3466b3B645C74aa7\",\n",
" \"makerAmount\": \"55000000000000000000\",\n",
" \"takerAmount\": \"1000000000000000000000\",\n",
" \"maker\": \"0x66d9357650beF62E9Ca5b7E250091Cf50D06413e\",\n",
" \"taker\": \"0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1\",\n",
" \"txOrigin\": \"0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1\",\n",
" \"pool\": \"0x0000000000000000000000000000000000000000000000000000000000000017\",\n",
" \"expiry\": \"1653688296\",\n",
" \"salt\": \"1622157380\",\n",
" \"chainId\": 3,\n",
" \"verifyingContract\": \"0xdef1c0ded9bec7f1a1670819833240f027b25eff\",\n",
" \"signature\": {\n",
" \"signatureType\": 3,\n",
" \"v\": 27,\n",
" \"r\": \"0x0b0c934656a8f29563b7962179602aa7e7184ea1abb8966dcda893bd0fc03298\",\n",
" \"s\": \"0x6c7181b5e3f2d29cd30e8cec13b7e949f559ec567f60056fe9217baaa939240c\"\n",
" }\n",
"}\n"
]
}
],
"source": [
"import json\n",
"\n",
"\n",
"def order_to_sra(order: RfqOrder, chain_id: int, verifying_contract: str, v: int, r: bytes, s: bytes):\n",
" obj = {\n",
" \"makerToken\": order.makerToken,\n",
" \"takerToken\": order.takerToken,\n",
" \"makerAmount\": str(order.makerAmount),\n",
" \"takerAmount\": str(order.takerAmount),\n",
" \"maker\": order.maker,\n",
" \"taker\": order.taker,\n",
" \"txOrigin\": order.txOrigin,\n",
" \"pool\": '0x' + order.pool.to_bytes(32, 'big').hex(),\n",
" \"expiry\": str(order.expiry),\n",
" \"salt\": str(order.salt),\n",
" \"chainId\": int(chain_id),\n",
" \"verifyingContract\": str(verifying_contract),\n",
" \"signature\": {\n",
" \"signatureType\": 3,\n",
" \"v\": v,\n",
" \"r\": f\"0x{r.hex()}\",\n",
" \"s\": f\"0x{s.hex()}\",\n",
" }\n",
" }\n",
" return json.dumps(obj, indent=4) # Indent is added simply for prettifying\n",
"\n",
"\n",
"print(order_to_sra(\n",
" order=order,\n",
" chain_id=3,\n",
" verifying_contract=ROPSTEN_EP,\n",
" v=v,\n",
" r=r,\n",
" s=s\n",
"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example responses for Firm and Indicative quotes on V4\n",
"\n",
"### V4 Indicative Quote\n",
"\n",
"When an indicative quote is requested and the request contains the argument `protocolVersion=4`, then we expect the following response:\n",
"\n",
"```json\n",
"{\n",
" \"makerToken\": \"0xF84830B73b2ED3C7267E7638f500110eA47FDf30\",\n",
" \"takerToken\": \"0x374a16F5E686c09B0CC9e8bC3466b3B645C74aa7\",\n",
" \"makerAmount\": \"1000000000000000000000\",\n",
" \"takerAmount\": \"1000000000000000000000\",\n",
" \"expiry\": \"1607996087\"\n",
"}\n",
"```\n",
"\n",
"### V4 Firm Quote\n",
"\n",
"When a firm quote is requested and the request contains the argument `protocolVersion=4`, then we expect the following response:\n",
"\n",
"```json\n",
"{\n",
" \"signedOrder\": {\n",
" \"makerToken\": \"0xF84830B73b2ED3C7267E7638f500110eA47FDf30\",\n",
" \"takerToken\": \"0x374a16F5E686c09B0CC9e8bC3466b3B645C74aa7\",\n",
" \"makerAmount\": \"1000000000000000000000\",\n",
" \"takerAmount\": \"1000000000000000000000\",\n",
" \"maker\": \"0x66d9357650beF62E9Ca5b7E250091Cf50D06413e\",\n",
" \"taker\": \"0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1\",\n",
" \"txOrigin\": \"0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1\",\n",
" \"pool\": \"0x0000000000000000000000000000000000000000000000000000000000000017\",\n",
" \"expiry\": \"1639089198\",\n",
" \"salt\": \"1607995204\",\n",
" \"chainId\": 3,\n",
" \"verifyingContract\": \"0x4cc72Bce22CfC7982BaB80B76a92Ac53698DC258\",\n",
" \"signature\": {\n",
" \"signatureType\": 3,\n",
" \"v\": 27,\n",
" \"r\": \"0xb7d8d73908472b43550542ee648721e156ae98029cb48da311b6aecd588ebdd0\",\n",
" \"s\": \"0x1e4ef1a050f846773efac14a71919870aa3b01937f3f586e497000d67f6221b5\"\n",
" }\n",
" }\n",
"}\n",
"```"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@PirosB3
Copy link
Author

PirosB3 commented Oct 11, 2022 via email

@jefesitosaul
Copy link

@PirosB3 which mainnets? ETH, BSC, POLY! Honestly, whichever MVP is quickest to get up and running IMO. Thanks for being a true 0X developer!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment