Skip to content

Instantly share code, notes, and snippets.

@lampshade9909
Forked from PirosB3/VolleyFireGuide.ipynb
Last active April 26, 2023 01:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lampshade9909/cf16b2aa198e4df8d00702ffd7bf506a to your computer and use it in GitHub Desktop.
Save lampshade9909/cf16b2aa198e4df8d00702ffd7bf506a to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![]()\n",
"\n",
"# 0xv4 RFQ order 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 = '0x4cc72Bce22CfC7982BaB80B76a92Ac53698DC258'\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: 9260686\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": "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": 4,
"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": 5,
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"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": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"RfqOrder(makerToken='0xF84830B73b2ED3C7267E7638f500110eA47FDf30', takerToken='0x374a16F5E686c09B0CC9e8bC3466b3B645C74aa7', makerAmount=1000000000000000000000, takerAmount=1000000000000000000000, maker='0x66d9357650beF62E9Ca5b7E250091Cf50D06413e', taker='0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1', txOrigin='0x06652BDD5A8eB3d206caedd6b95b61F820Abb9B1', pool=23, expiry=1639089198, salt=1607995204)"
]
},
"execution_count": 6,
"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=1639089198, # Some time in 2021\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(1000 * 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. @joey as you can see I copied a bit of your code as well as I've added some more code of mine. The function named `generate_order_hash_hex` in your code can be replaced with `get_order_hash_0xv4` even though it may require a few more trivial parameters. "
]
},
{
"cell_type": "code",
"execution_count": 7,
"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, private_key: str, 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",
" return keccak(\n",
" eip191_header\n",
" + eip712_domain_struct_hash\n",
" + eip712_order_struct_hash\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'c508e39e01419ba3b82eb51e2a2022e1a0498c8d422e09a6e6940650582f5d13'"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"off_chain_order_hash = get_order_hash_0xv4(order, 3, private_key_maker, ROPSTEN_EP)\n",
"off_chain_order_hash.hex()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Get RFQ Order Info\n",
"\n",
"As discussed, `filled` and `cancelled` functions on the exchange do not exist any more. We suggest to call `getRfqOrderInfo()` to get the same information that you need. You can read more about this [here](https://0xprotocol.readthedocs.io/en/latest/basics/functions.html#getrfqorderinfo)."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hash hex: c508e39e01419ba3b82eb51e2a2022e1a0498c8d422e09a6e6940650582f5d13\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.makerAmount,\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": 10,
"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": 11,
"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": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(27,\n",
" b'\\xb7\\xd8\\xd79\\x08G+CU\\x05B\\xeed\\x87!\\xe1V\\xae\\x98\\x02\\x9c\\xb4\\x8d\\xa3\\x11\\xb6\\xae\\xcdX\\x8e\\xbd\\xd0',\n",
" b'\\x1eN\\xf1\\xa0P\\xf8Fw>\\xfa\\xc1Jq\\x91\\x98p\\xaa;\\x01\\x93\\x7f?XnIp\\x00\\xd6\\x7fb!\\xb5')"
]
},
"execution_count": 12,
"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**. As discussed, the signature field passed into `fillRfqOrder` is now a struct instead of a bytes payload. I'm not sure that makes a difference when the data is encoded, but 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": 13,
"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": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'fillRfqOrder((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)'"
]
},
"execution_count": 14,
"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": 15,
"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.makerAmount,\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": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Fill transaction succeded: 0x5235bac14d5d843f598eb5a6ad643f2ef81de799488cd0898ea9d06ec508dadc\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": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[{'address': '0x4cc72Bce22CfC7982BaB80B76a92Ac53698DC258', 'topics': [HexBytes('0x829fa99d94dc4636925b38632e625736a614c154d55006b7ab6bea979c210c32')], 'data': '0xc508e39e01419ba3b82eb51e2a2022e1a0498c8d422e09a6e6940650582f5d1300000000000000000000000066d9357650bef62e9ca5b7e250091cf50d06413e00000000000000000000000006652bdd5a8eb3d206caedd6b95b61f820abb9b1000000000000000000000000f84830b73b2ed3c7267e7638f500110ea47fdf30000000000000000000000000374a16f5e686c09b0cc9e8bc3466b3b645c74aa7000000000000000000000000000000000000000000000000bb59a27953c60000000000000000000000000000000000000000000000000000bb59a27953c600000000000000000000000000000000000000000000000000000000000000000017', 'blockNumber': 9260688, 'transactionHash': HexBytes('0x5235bac14d5d843f598eb5a6ad643f2ef81de799488cd0898ea9d06ec508dadc'), 'transactionIndex': 3, 'blockHash': HexBytes('0xfd324e1a7652884fba3fdcf668105b01d210a3e5c97285c5feee05ab6d139f6e'), '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': ROPSTEN_EP})\n",
"print(logs)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Order hash: c508e39e01419ba3b82eb51e2a2022e1a0498c8d422e09a6e6940650582f5d13\n",
"Pool ID: 23\n",
"Taker performed a fill of 13.5 0x374a16f5e686c09b0cc9e8bc3466b3b645c74aa7 for 13.5 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. As discussed, 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": 19,
"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": 20,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Cancelled market for 0xF84830B73b2ED3C7267E7638f500110eA47FDf30<>0x374a16F5E686c09B0CC9e8bC3466b3B645C74aa7: 0x1b6894b70acc1ea918dac62a0e4cce72507a43e10028676086864d36252ce985\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": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\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"
]
}
],
"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",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"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.6.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@felixjff
Copy link

For anyone reading this doc in the future, there's a bug in fill_rfq_order and get_order_info. taker amounts are set to maker amounts. In this example it works, cause both taker and maker amounts are the same. If they would differ, the assert tests would fail.

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