examples/matching-engine/README.md
This sample application demonstrates a matching engine, showcasing the DeFi capabilities on the Linera protocol.
The matching engine trades between two tokens token_0 & token_1. We can refer to
the fungible application example on how to create two token applications.
An order can be of two types:
An OrderId is used to uniquely identify an order and enables the following functionality:
When inserting an order it goes through the following steps:
Transfer of tokens from the fungible application to the matching engine application through a cross-application
call so that it can be paid to the counterparty.
The engine selects the matching price levels for the inserted order. It then proceeds to clear these levels, executing trades and ensuring that at the end of the process, the best bid has a higher price than the best ask. This involves adjusting the orders in the market and potentially creating a series of transfer orders for the required tokens. If, after the level clearing, the order is completely filled, it is not inserted. Otherwise, it becomes a liquidity order in the matching engine, providing depth to the market and potentially being matched with future orders.
When an order is created from a remote chain, it transfers the tokens of the same owner
from the remote chain to the chain of the matching engine, and a ExecuteOrder message is sent with the order details.
Before getting started, make sure that the binary tools linera* corresponding to
your version of linera-sdk are in your PATH. For scripting purposes, we also assume
that the BASH function linera_spawn is defined.
From the root of Linera repository, this can be achieved as follows:
export PATH="$PWD/target/debug:$PATH"
source /dev/stdin <<<"$(linera net helper 2>/dev/null)"
Next, start the local Linera network and run a faucet:
FAUCET_PORT=8079
FAUCET_URL=http://localhost:$FAUCET_PORT
linera_spawn linera net up --with-faucet --faucet-port $FAUCET_PORT
# If you're using a testnet, run this instead:
# LINERA_TMP_DIR=$(mktemp -d)
# FAUCET_URL=https://faucet.testnet-XXX.linera.net # for some value XXX
Enable logs for user applications:
export LINERA_APPLICATION_LOGS=true
Create the user wallet and add chains to it:
export LINERA_WALLET="$LINERA_TMP_DIR/wallet.json"
export LINERA_KEYSTORE="$LINERA_TMP_DIR/keystore.json"
export LINERA_STORAGE="rocksdb:$LINERA_TMP_DIR/client.db"
linera wallet init --faucet $FAUCET_URL
INFO_1=($(linera wallet request-chain --faucet $FAUCET_URL))
INFO_2=($(linera wallet request-chain --faucet $FAUCET_URL))
INFO_3=($(linera wallet request-chain --faucet $FAUCET_URL))
CHAIN_1="${INFO_1[0]}"
CHAIN_2="${INFO_2[0]}"
CHAIN_3="${INFO_3[0]}"
OWNER_1="${INFO_1[1]}"
OWNER_2="${INFO_2[1]}"
OWNER_3="${INFO_3[1]}"
Publish and create two fungible applications whose IDs will be used as a
parameter for the matching-engine application:
FUN1_APP_ID=$(linera --wait-for-outgoing-messages \
project publish-and-create examples/fungible \
--json-argument "{ \"accounts\": {
\"$OWNER_1\": \"100.\",
\"$OWNER_2\": \"150.\"
} }" \
--json-parameters "{ \"ticker_symbol\": \"FUN1\" }" \
)
FUN2_APP_ID=$(linera --wait-for-outgoing-messages \
project publish-and-create examples/fungible \
--json-argument "{ \"accounts\": {
\"$OWNER_1\": \"100.\",
\"$OWNER_2\": \"150.\"
} }" \
--json-parameters "{ \"ticker_symbol\": \"FUN2\" }" \
)
The flag --wait-for-outgoing-messages waits until a quorum of validators has confirmed
that all sent cross-chain messages have been delivered.
Now, we publish and deploy the Matching Engine application:
MATCHING_ENGINE=$(linera --wait-for-outgoing-messages \
project publish-and-create examples/matching-engine \
--json-parameters "{\"tokens\":["\"$FUN1_APP_ID\"","\"$FUN2_APP_ID\""]}" \
--required-application-ids $FUN1_APP_ID $FUN2_APP_ID)
First, a node service for the current wallet has to be started:
PORT=8080
linera service --port $PORT &
Type each of these in the GraphiQL interface and substitute the env variables with their actual values that we've defined above.
Navigate to the URL you get by running echo "http://localhost:8080/chains/$CHAIN_1/applications/$MATCHING_ENGINE":
To create a Bid order as owner 1, offering to buy 1 FUN1 for 5 FUN2:
mutation {
executeOrder(
order: {
Insert : {
owner: "$OWNER_1",
amount: "1",
nature: Bid,
price: {
price:5
}
}
}
)
}
To query about the bid price:
query {
bids {
keys {
price
}
}
}
In general, if you send tokens to a chain owned by someone else, you rely on them for asset availability: If they don't handle your messages, you don't have access to your tokens.
Fortunately, Linera provides a solution based on temporary chains: If the number of parties who want to swap tokens is limited, we can make them all chain owners, allow only Matching Engine operations on the chain, and allow only the Matching Engine to close the chain.
kill %% && sleep 1 # Kill the service so we can use CLI commands for chain 1.
linera --wait-for-outgoing-messages change-ownership \
--owners "{\"$OWNER_1\":100,\"$OWNER_2\":100}"
linera --wait-for-outgoing-messages change-application-permissions \
--execute-operations "[\"$MATCHING_ENGINE\"]" \
--close-chain "[\"$MATCHING_ENGINE\"]"
linera service --port $PORT &
First, owner 2 should claim their tokens. Navigate to the URL you get by running echo "http://localhost:8080/chains/$CHAIN_2/applications/$FUN1_APP_ID":
mutation {
claim(
sourceAccount: {
chainId: "$CHAIN_1",
owner: "$OWNER_2",
}
amount: "100.",
targetAccount: {
chainId: "$CHAIN_2",
owner: "$OWNER_3"
}
)
}
And to the URL you get by running echo "http://localhost:8080/chains/$CHAIN_2/applications/$FUN2_APP_ID":
mutation {
claim(
sourceAccount: {
chainId: "$CHAIN_1",
owner: "$OWNER_2",
}
amount: "150.",
targetAccount: {
chainId: "$CHAIN_2",
owner: "$OWNER_2"
}
)
}
Owner 2 offers to buy 2 FUN1 for 10 FUN2. This gets partially filled, and they buy 1 FUN1
for 5 FUN2 from owner 1. This leaves 5 FUN2 of owner 2 on chain 1. On the URL you get by running echo "http://localhost:8080/chains/$CHAIN_2/applications/$MATCHING_ENGINE":
mutation {
executeOrder(
order: {
Insert : {
owner: "$OWNER_2",
amount: "2",
nature: Ask,
price: {
price:5
}
}
}
)
}
The only way to close the chain is via the application now. On the URL you get by running echo "http://localhost:8080/chains/$CHAIN_1/applications/$MATCHING_ENGINE":
mutation { closeChain }
Owner 2 should now get back their tokens, and have 145 FUN2 left. On the URL you get by running echo "http://localhost:8080/chains/$CHAIN_2/applications/$FUN2_APP_ID"
query {
accounts {
entry(
key: "$OWNER_2"
) {
value
}
}
}