Specification document (this README), design screenshots, API reference, code samples, and the master sketch file for the Paradigm/Kosu search portal.
The search portal enables users to view and fill 0x orders that have been relayed through the Kosu network. It provides a simple interface to query orders across token pairs for bids and asks, and to sort by price and size.
The portal relies on an external RPC API server to act as a bridge between the Kosu network and clients. This API server is not yet built, and the underlying Kosu network is not yet relaying orders. However, the REST API for the RPC server is defined here, and it will eventually be built to meet this spec.
The specification section contains screenshots of relevant states, however the full sketch file should still be reviewed in detail.
- Basic understanding of the 0x system (link).
- Basic understanding of
web3
and Ethereum (link). - Usage of the
0x.js
library and other 0x tools (link). - Understanding of quote vs. base currency (link).
- Understanding of bids vs. asks and relation to currency pair (link).
- Understanding of basic order book concepts (link).
- Use
BigNumber
instances for balances and allowances (can be imported from0x.js
). - Remember to convert to/from
wei
units where necessary (more info). - The portal is built for "standard tokens" (WETH, DAI, & ZRX) whose addresses can be found here.
Also see standard tokens.
Live assets, requires real Ether and tokens to fill orders, production only.
- Network ID:
1
- Custom RPC:
https://ethnet.zaidan.io/mainnet
(or use Metamask default)
Private Paradigm development network for testing with 0x contracts pre-deployed (uses the 0x ganache snapshot.). Reach out for test tokens and Ether.
- Network ID:
50
- Custom RPC:
https://ethnet.zaidan.io/zrx
This section describes the necessary functionality of the search portal, and maps design screenshots to the functionality that must be implemented for each section, based on the code samples and external API.
The Kosu search portal essentially consists of one page that offers a form to search for and load orders from an external order book. Loaded orders offer a "take" button to submit the order to the Ethereum blockchain for settlement via the 0x smart contract pipeline.
It offers the ability to set allowances for the 0x proxy contract prior to settlement, depending on weather or not the user already has an allowance set.
The states in this section indicate the default state of the portal as displayed to the user on load.
Prior to interacting with any functionality of the portal, the user must allow the website access to their Metamask account by clicking "Connect to Metamask."
- The default state is to load bids for the WETH/DAI pair.
- Prior to successful connection, the "take" action on loaded orders should be disabled.
- When Metamask is not connected, the user should be prompted to connect (top-right).
- Clicking "connect to metamask" should trigger the connection function.
- The initialized
web3
and necessary 0x contract instances should be saved to accessible state.
- After successful connection (connection function resolves with no error) the user's
coinbase
address should be displayed. - Additionally, their DAI balance (and USD conversion) should be displayed as well.
- The top navigation bar always show's the user's
coinbase
address, and balance of the quote token. - If the base and quote tokens are switched (via "token pair", or the switch button) the displayed balance should update to the new quote asset.
- Balances for tokens can be loaded with a method like this example.
- For standard tokens, the address can be loaded from an in-state mapping.
- For custom tokens, the address can be used directly, after validation.
Trading within the 0x ecosystem requires setting an "allowance" for a smart contract that helps facilitate the trade of tokens. This is called a proxy allowance, and must be set for each token in the trading pair (the selected base and quote asset)
Each time the a token within the "token pair" is changed, the user's proxy allowances should be checked for each token in the pair (by address).
If an allowance must be set for one of the tokens in the pair, the user should be prompted to set an unlimited proxy allowance for that token's address.
If more than one (i.e. both) assets in the pair do not have sufficient allowances set, the prompt to set an allowance for each should be displayed (see below). The user should be prompted to set the an allowance for the base asset first, then the quote asset.
- If there is no proxy allowance for a standard token, this state can be shown.
- The button "set {TICKER} allowance" should trigger setting an unlimited proxy allowance for that token address.
- If there is no proxy allowance for a custom token, this state can be shown.
- The button "set {ADDRESS} allowance" should trigger setting an unlimited proxy allowance for that token address, after the custom address is validated.
- The full address should not be shown, but instead concatenated as
0x14d..35a
(for example).
The filter form section allows the user to query for fillable orders from the Kosu network, based on a token pair (base/quote token) and a side (either "bid" or "ask").
Each time the form is updated, a matching query should be made to the REST API server, based on the state of the form.
A new query should be made (and the order table updated) whenever:
- A new base asset is selected via drop down
- A new quote asset is selected via drop down
- A new custom base token address is entered and validated as an Ethereum address
- A new custom quote token address is entered and validated as an Ethereum address
- The base and quote asset are switched (switch button)
- The "bid/ask" toggle is switched
Additionally, whenever a new base or quote asset is selected, allowances must be checked for the newly selected token(s). See the allowance section as well.
- Some common ERC-20 tokens are available for selection via a drop down menu.
- Both a base (leftmost, WETH here) and quote (rightmost, DAI here) asset can be selected.
- This state shows a standard token loaded for both base and quote asset.
- Notice the quote asset's balance (DAI) is displayed to the user.
- The "bid/ask" toggle can be flipped.
- Additionally, the base and quote asset can be switched with the circular arrow button (requires order table update too).
- When toggled, a new call to
/search
should be made, triggering the order table to update.
- The drop down menu for quote and base tokens allows selection of pre-set standard tokens and custom.
- Base and quote currency must be different, so if a user tries to set the quote token for what is currently the base token, base and quote should be switched (same as clicking switch button).
- The "custom" selection allows a user to enter a custom ERC-20 token address (see here).
- Whenever a new token is selected for either base or quote:
- Allowances must be checked for the new token.
- A new REST API call must be made to
/search
with the new token(s).
- This state shows a standard token as the base token, and a custom token as the quote token.
- Note that displayed prices and sizes in the order table now show a shortened address instead of a standard token ticker.
- When custom is selected, a new API call should not be made until the user has entered a custom token address and it has been validated.
- This state shows a custom token selected for both the base and quote tokens.
- Note that displayed prices and sizes in the order table now show a shortened address instead of a ticker from a standard token.
- When custom is selected, a new API call should not be made until the user has entered a custom token address and it has been validated.
- In this case, a new API call should only be made after both custom token addresses have been entered and validated.
- The "order source" selection can currently only be 0x orders.
- The other selections (dY/dX and Dharma) should be disabled.
- If the user hovers over those options, this tooltip can be displayed.
The order table is where quote snapshots are loaded from the external API, based on the form values selected by the user.
The quotes displayed in the table are based on response objects from the the /search
API method, and if the user clicks "take" on a given quote, the full order is loaded with the /order
method.
Each time the form is updated, a new set of quotes should be loaded from the API.
- If allowances are correctly set, the "take" button can be displayed to user's for each quote.
- Clicking "take" should trigger the following actions:
- The button should display the animated loading icon while the following asynchronous actions complete.
- Load the full order based on the quote's
orderId
from the/order
method. - Check the order's status to ensure it is fillable.
- Verify the fill will succeed if submitted on chain.
- After the above completes, the button should display "confirm" (see next state).
- If the validation fails, show the failed state.
- After the pre-fill checks complete (triggered by user clicking "take") they are prompted to confirm the fill.
- If the user clicks "confirm" again, the following should take place:
- TThe button should display the animated loading icon while the following asynchronous actions complete.
- The fill should be executed, triggering a Metamask prompt to the user.
- The loading animation should continue to display while the transaction awaits confirmation.
- When the promise returned from
awaitTransactionSuccess
resolves, the button should display "taken" (see here).
- If the fill fails, show the failed fill state.
- This state is displayed while async validation and async execution occur.
- The displayed icon in the button should rotate to indicate pending requests and transactions.
- This state can be displayed when a fill completes successfully.
- Clicking on the "taken" button should take the user to the Etherscan TX page corresponding to the fill transaction ID.
- This state can be displayed when a fill or validation fails when attempting to take an order.
- If the failure is during the fill execution, no link is needed.
- If the failure is during awaiting the fill being mined, the button can link to Etherscan for that transaction ID.
- If the failure is during fill validation, no link is needed.
API reference for a future middleware server that will respond to client requests from a database of Kosu orders.
Load and paginate an order-book snapshot of quotes rom the Kosu network by supplying a baseAsset
address, quoteAsset
address, and a side
(bid/ask).
The returned quotes include orderId
values which can be used to load the full executable order with the /order
method.
-
API Path:
/search
-
Query Parameters:
Name Required Default Description baseAsset
true
- Base asset token address. quoteAsset
true
- Quote asset token address. side
true
- Specify to retrieve bid
orask
orders for the pair.page
false
1
The page number to retrieve (based on perPage
).perPage
false
10
The number of order stubs to load per page. -
Example:
curl 'https://search.zaidan.io/api/v1/search?baseAsset=0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2"eAsset=0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359&side=ask&perPage=2'
- Headers:
- Content-Type:
application/json
- Content-Type:
- Body:
{ "side": "ask", "quoteAssetAddress": "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", "baseAssetAddress": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359", "page": 1, "perPage": 2, "quotes": [ { "price": "21624920000000", "size": "534680005130000000", "expiration": "1561496835", "orderId": "0x012761a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33" }, { "price": "216249200000000000", "size": "53468000513000000000", "expiration": "1561497137", "orderId": "0x013842a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b3518891" } ] }
- Notes:
- Expiration times are UNIX timestamps (seconds).
- Expiration times should be converted to JavaScript
Date
objects. - Prices and sizes (
price
andsize
) are in base units (wei) of the quote asset. - Prices and order sizes should be converted to
BigNumbers
for storage/processing (for precision). - The
orderId
for an quote can be used in the/order
method to get the full order.
Load a full 0x order object from the Kosu network, provided an orderId
string.
-
API Path:
/order
-
HTTP Method:
GET
-
Query Parameters:
Name Required Default Description id
true
- The hex-encoded transaction ID of the order to fetch. -
Example:
curl 'https://search.zaidan.io/api/v1/order?id=0x3b5d97f1a8d0eb833fe1954f87ec3e8099a1d012f5aac397c987b414060546af'
- Headers:
- Content-Type:
application/json
- Content-Type:
- Body:
{ "id": "0x3b5d97f1a8d0eb833fe1954f87ec3e8099a1d012f5aac397c987b414060546af", "order": { "makerAddress": "0xa916b82ff122591cc88aac0d64ce30a8e3e16081", "makerAssetAmount": "1000000000000000000", "takerAssetAmount": "1000000000000000000", "expirationTimeSeconds": "1559941224", "makerAssetData": "0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498", "takerAssetData": "0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359", "makerFee": "0", "takerFee": "0", "salt": "45038821417800674048750115101428369947416636882675537172847246510449321143785", "exchangeAddress": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b", "takerAddress": "0x0000000000000000000000000000000000000000", "feeRecipientAddress": "0x0000000000000000000000000000000000000000", "senderAddress": "0x0000000000000000000000000000000000000000", "signature": "0x1cfab1d9c5df24fa0f74f274b4e0668735bfd9faf029448b6925b795f3a97ce75826bbdfdfaad7eb40692e239726dfc36d74e740e579cb561cd6a798ad92921c4202" } }
JavaScript code samples for working with the 0x.js
and web3
libraries for checking balances and filling orders.
Most of the function implementations can be copy/pasted, except for the loading of required "globals" such as initialized ContractWrapper
and Web3
instances. These should be loaded from front-end state management (redux) and initialized/saved during the "connect to metamask" saga.
Use the following sample to connect to Metamask and load the user's coinbase
address. This action should be triggered by a user clicking the "Connect to MetaMask" button. If the following throws, it can be assumed the user is in an incompatible browser (or doesn't have Metamask installed).
The web3
instance and coinbase
address should be saved and accessible throughout the application.
import Web3 from "web3";
async function connectMetamask() {
if (window.ethereum !== void 0) {
try {
// will prompt user with pop-up to allow site access
await window.ethereum.enable();
// load this web3 into state somewhere (needed later)
web3 = new Web3(window.ethereum);
// additionally, store the users address somewhere
coinbase = await web3.eth.getCoinbase();
} catch (error) {
throw new Error("user denied site access");
}
} else if (window.web3 !== void 0) {
// same as above, var (or let) scoped, or stored in redux state
web3 = new Web3(web3.currentProvider);
coinbase = await web3.eth.getCoinbase();
// optional
global.web3 = web3;
} else {
throw new Error("non-ethereum browser detected");
}
}
During main initialization, when Metamask is connected, some 0x contract code used to verify and fill trades must be initialized. The objects indicated below must be saved and accessible through the applications state, just like the web3
instance.
import { Web3Wrapper } from "@0x/web3-wrapper";
import { ContractWrappers } from "0x.js";
// passed in `web3` should be from Metamask initialization
async function initZeroEx(web3) {
const networkId = await web3.eth.net.getId();
// save the following two objects in application state
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const contractWrappers = new ContractWrapper(web3Wrapper.getProvider(), { networkId });
return {
web3Wrapper,
contractWrappers
};
}
The user's coinbase
address may be loaded any time from an initialized web3
instance. It should be stored in-state for easy access.
async function getCoinbase() {
const coinbase = await web3.eth.getCoinbase();
return coinbase;
}
Prior to verifying or actually filling an order as a taker, the maker order must be checked for its fillable status.
// pseudocode - should be loaded from redux state
const { contractWrappers } = state;
// the order object should come from API call
async function isFillable(order) {
const info = await contractWrappers.exchange.getOrderInfoAsync(order);
if (info.orderStatus !== 2) {
return false;
}
return true;
}
Before actually submitting an order for settlement, the trade can be "simulated" using the 0x libraries to ensure it will fill successfully.
import { BigNumber } from "0x.js";
// pseudocode - should be loaded from redux state
const { contractWrappers } = state;
// order should come from API call, taker should be the user's coinbase address
// if this function *does not* throw, the fill can be assumed to succeed
async function validateFill(order, taker) {
const takerAmount = new BigNumber(order.takerAssetAmount);
try {
await contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync(
order,
takerAmount,
taker
);
} catch (error) {
throw new Error(`validate fill failed: ${error.message}`);
}
}
After all verification steps have passed, and the user clicks the "confirm" button, the order can be settled on-chain through the 0x smart contracts.
Unlike the demo method for setting ERC-20 proxy allowances, this method will resolve after the transaction is signed and submitted to the transaction ID. To wait for the fill to be mined (i.e. for displaying the pending icon) use the demo method for awaiting transaction success.
import { BigNumber } from "0x.js";
// pseudocode - should be loaded from redux state
const { contractWrappers, web3 } = state;
// order should come from API call, taker should be the user's coinbase address
async function fillOrder(order, taker) {
let hash; // will store order txId for viewing on Etherscan
const takerAmount = new BigNumber(order.takerAssetAmount);
try {
hash = await contractWrappers.exchange.fillOrderAsync(
order,
takerAmount,
taker,
{
gasPrice: await getGasPrice(web3)
},
);
} catch (error) {
throw new Error(`fill failed: ${error.message}`);
}
return hash;
}
Before a user can fill a trade (the "take" button) they must have an allowance set for the 0x proxy contract for each token in the pair. This function can be used to check if setting proxy allowances is necessary for either token in the pair.
The user must have an allowance equal to or greater than the takerAssetAmount
of the maker order they are trying to fill.
// pseudocode - load initialized object from redux state
const { contractWrappers } = state;
// returns a BigNumber instance with the current proxy allowance for tokenAddress
async function getProxyAllowance(tokenAddress, userAddress) {
let allowance;
try {
allowance = await contractWrappers.erc20Token.getProxyAllowanceAsync(
tokenAddress,
userAddress,
);
} catch (error) {
throw new Error(`unable to get token proxy allowance: ${error.message}`);
}
return allowance;
}
Prior to participating in trades through the 0x contract system, the taker (the user) must set token allowances for the 0x proxy contract. An allowance for the 0x proxy contract must be set for each token (base/quote) prior to filling a trade.
This implementation (can be copy/pasted) only resolves after allowance tx is successful and will throw otherwise.
// pseudocode - load initialized objects from redux state
const { contractWrappers, web3Wrapper } = state;
// tokenAddress should be the 0x-prefixed Ethereum address of the token to set an allowance for
// userAddress should be the 0x-prefixed Ethereum address of the user (coinbase)
async function setUnlimitedProxyAllowance(tokenAddress, userAddress) {
// first, prompt user to sign and submit
let txId;
try {
txId = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync(
tokenAddress,
userAddress,
);
} catch (error) {
// user probably denied transaction
throw new Error(`failed to submit allowance transaction: ${error.message}`);
}
// second, wait for the allowance transaction to be mined successfully
try {
await web3Wrapper.awaitTransactionSuccessAsync(txId);
} catch (error) {
throw new Error(`allowance transaction failed: ${error.message}`)
}
return;
}
This function can be used to check a user's balance for any ERC20 token, provided a 0x-prefixed token address. The value is returned as a BigNumber
in units of wei
(base token units).
// pseudocode - load initialized objects from redux state
const { contractWrappers } = state;
// tokenAddress is the ERC-20 token address, userAddress is the user's coinbase
async function getTokenBalance(tokenAddress, userAddress) {
let balance;
try {
balance = await contractWrappers.erc20Token.getBalanceAsync(
tokenAddress,
userAddress,
);
} catch (error) {
throw new Error(`unable to get token balance: ${error.message}`);
}
return balance;
}
This method can be used to return a promise that resolves when a transaction with the provided txId
is mined in an Ethereum block.
// pseudocode - load initialized objects from redux state
const { web3Wrapper } = state;
// 32-byte (0x-prefixed, 66-char) hex encoded transaction hash
async function awaitTransactionSuccess(txId) {
if (!/^0x[a-fA-F0-9]{64}$/.test(txId)) {
throw new Error("invalid transaction hash");
}
try {
await web3Wrapper.awaitTransactionSuccessAsync(txId);
} catch (error) {
throw new Error(`transaction failed: ${error.message}`);
}
}
An Ethereum address is a 20-byte identifier used to receive assets on the Ethereum network, and is usually represented as a 42-character ("0x" prefixed) hexadecimal encoded string. They can be validated with a simple regular expression.
// valid addresses are 42 character hex-encoded strings (20 bytes, add 0x prefix)
// the regex used in this method accepts check-summed and non-check-summed addresses
function isValidAddress(maybeAddress) {
if (typeof maybeAddress !== "string") {
return false;
}
return /^0x[a-fA-F0-9]{40}$/.test(maybeAddress);
}
In order to prevent slow (or expensive) transactions, a reasonable gas price should be used for all fill transactions. This value can be loaded from the https://ethgasstation.info
website.
// provide instantiated web3 instance, returns BigNumber
async function getGasPrice(web3) {
// get a reasonable gas price, use 10 if API fails
const rawRes = await fetch("https://ethgasstation.info/json/ethgasAPI.json");
const parsed = await rawRes.json();
const gasPriceGwei = parsed["safeLow"] ? parsed["safeLow"].toString() : "10";
return new BigNumber(web3.utils.toWei(gasPriceGwei, "Gwei"));
}
The portal supports a few common tokens, who's addresses should be either hard-coded or loaded from the included common-tokens.json
file.
The first level in the object is the network ID, so it may be used as follows:
// or hard-code into common/utility object
const COMMON_TOKENS = require("./common-tokens.json");
// pseudocode -- use redux state, etc.
const { web3 } = state;
// returns the ERC-20 token's address for a supported common ticker, based on the detected networkId
function getCommonTokenAddress(ticker) {
// or use `await web3Wrapper.getNetworkId()`
const networkId = await web3.eth.net.getId();
if (!COMMON_TOKENS[networkId]) {
throw new Error("unsupported network");
}
const address = COMMON_TOKENS[networkID][ticker];
if (!address) {
throw new Error("invalid common token ticker");
}
return address
}
{
"1": {
"WETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"DAI": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359",
"ZRX": "0xe41d2489571d322189246dafa5ebde1f4699f498"
},
"50": {
"WETH": "0x34d402f14d58e001d8efbe6585051bf9706aa064",
"DAI": "0x25b8fe1de9daf8ba351890744ff28cf7dfa8f5e3",
"ZRX": "0xcdb594a32b1cc3479d8746279712c39d18a07fc0"
}
}