Snippets
Created by
Riccardo Sibani
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | /* TODO
*
* - Set unique Key in classes
*/
object ft3_config {
blockchain_name: text = 'ChromaDeX';
blockchain_website: text = 'https://dex.chroma.dev';
blockchain_description: text = 'description';
}
//include "lib/main";
namespace ft3 {
include "lib/ft3/account/main";
include "lib/ft3/account/auth_basic";
include "lib/ft3/asset";
include "lib/ft3/dev_op";
include "lib/ft3/history";
include "lib/ft3/transfer";
include "lib/ft3/blockchain";
}
/* DEX_lib
* this library is only meant for fungible tokens
* it is compatible (and requires) with FT3_lib
*/
object blockchain_info {
chain_rid: byte_array = x"AAAAAAAAAA";
}
/* the assets hold by this balance are locked and can't be exchanged by ft3
* Of course they can be freely transfer back and forth to ft3 balance
* I considered creating a particular descriptor but this would mean introducing a style where there are many different type of accounts
* and also create an arbitrary amount of "flags" in FT3. While this seem like a sensible approach (i.e. you can trade in the market only
* with that descriptor), it has the problem that we should hard code the verification in ft3_lib (check_auth_args). We might come up with
* Something more flexible in the future
*/
// balance hodl by the exchange_account on this chain (there is no need to save the chain_id in this account as there is no way this
// balance exits the blockchain)
// TODO OPTION, this might not even be needed and transfer directly from ft3
class tradable_balance {
asset: ft3.asset;
account: ft3.account;
key account, asset;
mutable amount: integer;
}
/* PAIRING
* What asset is the user giving away and what asset it wants in exchange.
* We should store all the orders of one pair as list from where we fulfill the olders first
*/
class pair {
from: ft3.asset;
to: ft3.asset;
}
class order_log (log) { // This means one order per block. Seems like a (un)reasonable ddos prevention (of course I can have many accounts and go around it), but it's something. We could also accept a list of
index account: ft3.account;
index block_height: integer; // I index it as it seems that block height is not indexed in db
}
class order {
index id: byte_array;
index account: ft3.account;
index pair;
index rate: decimal;
mutable amount_from: integer;
partial_fulfill: boolean;
mutable active: boolean = true; // maybe this can be removed, it's just amount_from >0
order_log;
index amount_from;
index active;
// Perhaps add a TTL to the order? (Time to Live)
}
class trade_log (log) {
trade_id: byte_array;
order;
amount: integer;
is_matched: boolean; // if the order was alrteady in the queue and has been matched through the match_order algorithm, set it to true
}
function roundRate (rate: decimal): decimal { return rate.round(5); }
function ensure_pair(from: ft3.asset, to: ft3.asset): pair {
val is_pair = pair@?{.from == from, .to == to};
return is_pair?: create pair(.from = from, .to = to);
}
function get_orders(pair, rate: decimal, from_block: integer, num_of_blocks: integer): list<order> {
// 1. get num_of_blocks blocks back (e.g. num_of_blocks = 25, we want to get the next 25 blocks with at least one order inside each one of them
// 2. get those orders and send them
// NOTE: with `distinct block_height` this would be pretty easy.
log("A");
val blocks = set<integer>();
var start = from_block;
while (blocks.size() < num_of_blocks and start < from_block+num_of_blocks) {
blocks.add_all(
order@*{
pair,
.rate >= rate,
.active == true,
.order_log.block_height >= start,
.order_log.block_height < start + num_of_blocks
}(-sort .order_log.block_height)
);
if(blocks.size() < num_of_blocks) {
start += num_of_blocks;
}
}
log("B");
// convert the set of blocks into a list so I can get the lower and higher
val blocks_list = blocks.sorted();
// not optimal, there should be a way to return the order itself
val orders_sorted = order@*{
pair,
.rate >= rate,
.active == true,
.order_log.block_height >= blocks_list[0],
.order_log.block_height <= blocks_list[blocks_list.size() - 1]
}(-sort .order_log.block_height, order = order);
log("C");
// cleaning the tuple(block_height, order) to a list of orders that I'll return
// I could skip it and just return the tuple...
val orders = list<order>();
for (order_sorted in orders_sorted) {
orders.add(order_sorted.order);
}
log("D");
return orders;
}
function matchOrder(order_to_fulfill: order) {
val reversed_pair = ensure_pair(order_to_fulfill.pair.to, order_to_fulfill.pair.from);
val last_block = order@{reversed_pair}(-sort .order_log.block_height) limit 1; // get the last block with and order with the reversed pair, so I know when to stop
var current_block = 0;
var finish = false;
var expectedRate = roundRate(1/order_to_fulfill.rate);
var step = 25; // This could be parametrically changed
var amount_to_fulfill = integer(order_to_fulfill.amount_from * order_to_fulfill.rate);
val full_matching_orders = list<order>();
var partially_matched_order: order? = null;
while (current_block <= last_block and not finish) {
log("current block", current_block);
log("last block", last_block);
val orders_to_eval = get_orders(reversed_pair, expectedRate, current_block, step);
log(1);
for(eval_order in orders_to_eval) {
if(eval_order.amount_from <= amount_to_fulfill) {
log(2);
// add the order to the ones that match
full_matching_orders.add(eval_order);
amount_to_fulfill -= eval_order.amount_from;
finish = (amount_to_fulfill == 0);
} else if (eval_order.partial_fulfill == true) {
log(3);
partially_matched_order = eval_order;
// amount_to_fulfill is not equal to the amount that needs to be taken by the partial matched_order
finish = true;
}
}
current_block += orders_to_eval[orders_to_eval.size() -1].order_log.block_height;
}
if(finish or order_to_fulfill.partial_fulfill) {
// consume all the orders in full
for(full_matching_order in full_matching_orders) {
val amountToken1 = integer(full_matching_order.amount_from * full_matching_order.rate);
consume_order(order_to_fulfill, full_matching_order, amountToken1, full_matching_order.amount_from);
full_matching_order.active = false; // set active to false as it is fully fulfilled
}
// consume the partial fulfill order
if(partially_matched_order != null) {
val amountToken1 = integer(amount_to_fulfill * partially_matched_order.rate);
consume_order(order_to_fulfill, partially_matched_order, amountToken1, amount_to_fulfill);
}
require(order_to_fulfill.amount_from == 0 or order_to_fulfill.partial_fulfill, "Seems like the alg thinks it can fulfill your order but couldn't. Maybe something went wrong with token convestion * exchangeRate?" );
order_to_fulfill.active = order_to_fulfill.amount_from > 0;
}
}
function decrease_tradable_balance(account: ft3.account, asset: ft3.asset, amount: integer) {
// Remove amount from tradable balance
val tb = tradable_balance@{account, asset};
tb.amount -= amount;
// Get all the orders with the Pair with asset updated in selling position, and remove (or decrease if partial fulfill) if amount to sell > amount left
// So...
// 1. delete all the orders with less than amountLeft, anyway (should I add log?)
delete order@*{account, .pair.from == asset, .amount_from < tb.amount};
// 2. If partial_fullfill update to new possible balance
update order@*{
account,
.pair.from == asset,
.partial_fulfill == true
}(.amount_from = tb.amount);
}
function consume_order(order_to_match: order, matched_order: order, amount_from: integer, amount_to: integer) {
val exchange_log_id = (op_context.transaction.tx_rid, order_to_match.id, matched_order).hash();
create trade_log(exchange_log_id, order_to_match, amount_from, false);
create trade_log(exchange_log_id, matched_order, amount_to, true);
// decrease left amount from toSatisfy order
order_to_match.amount_from -= amount_from;
// decrease money from tradable balance
decrease_tradable_balance(order_to_match.account, order_to_match.pair.from, amount_from);
// decrease left amount from matched order
matched_order.amount_from -= amount_to;
// decrease money from tradable balance
decrease_tradable_balance(matched_order.account, matched_order.pair.from, amount_to);
}
record orderRequest {
account_id: byte_array;
auth_descriptor_id: byte_array;
asset_from_id: byte_array;
amount_from: integer;
asset_to_id: byte_array;
rate: decimal;
partial_fulfill: boolean;
}
operation addOrder(order: orderRequest) {
_addOrder(order);
}
function _addOrder (order: orderRequest) {
require(order.amount_from > 0, "You can't buy 0 or less");
require(order.rate > 0, "You can't trade at negative rate");
val account = ft3.account@{order.account_id};
// This is a standard operation so reqire_auth is enough
ft3.require_auth(account, order.auth_descriptor_id, list<text>());
val asset_from = ft3.asset@{.id == order.asset_from_id};
val asset_to = ft3.asset@{.id == order.asset_to_id};
val order_at_price = create order (
.id = op_context.transaction.tx_rid.hash(),
account,
ensure_pair(asset_from, asset_to),
rate = roundRate(order.rate),
.amount_from = order.amount_from,
.partial_fulfill = order.partial_fulfill,
create order_log(account, op_context.block_height)
);
matchOrder(order_at_price);
}
// create tradable_balance if doesn't exist
function ensure_tradable_balance(acc: ft3.account, asset: ft3.asset): tradable_balance {
val is_tradable_balance = tradable_balance@?{acc, asset};
return is_tradable_balance?: create tradable_balance(acc, asset, 0);
}
function deduct_tradable_balance(acc: ft3.account, asset: ft3.asset, amount: integer) {
val b = tradable_balance@{acc, asset};
require(b.amount >= amount, "Tradable balance is too low");
b.amount -= amount;
}
function consume_exchange_input(i: ft3.xfer_input, idx: integer, assets: map<ft3.asset, integer>): ft3.payment_history_entry {
val asset = ft3.asset@{ .id == i.asset_id};
val acc = ft3.account@{i.account_id};
ft3.require_auth(acc, i.auth_descriptor_id, ["T"]);
require(i.amount >= 0, "Amount should be positive");
deduct_tradable_balance(acc, asset, i.amount);
assets[asset] = i.amount + if (asset in assets) assets[asset] else 0;
return create ft3.payment_history_entry (
acc,
asset,
.delta = i.amount,
.op_index = 500, //op_context ???
.is_input = true,
.entry_index = idx
);
}
// process the transfer to exchange, saving the balance in tradable balance allows to 'hide' the balance to ft3 and therefore not transfer it
function process_exchange_output(o: ft3.xfer_output, idx: integer, available_assets: map<ft3.asset, integer>): ft3.payment_history_entry {
require(o.amount >= 0, "Amount should be positive");
if (o.extra.contains("reg_auth_desc")) {
val auth_desc = ft3.auth_descriptor.from_gtv(o.extra["reg_auth_desc"]);
require(o.account_id
== ft3.create_account_with_auth(auth_desc).id);
}
val acc = ft3.account@{.id == o.account_id};
val asset = ft3.asset@{.id == o.asset_id};
require(available_assets[asset] >= o.amount, "Not enough balance received");
available_assets[asset] -= o.amount;
val tradable_balance = ensure_tradable_balance(acc, asset);
tradable_balance.amount += o.amount;
return create ft3.payment_history_entry (
.account = acc,
asset,
.delta = o.amount,
.op_index = 500, // something from op_context,
.is_input = false,
.entry_index = idx
);
}
// Similar to ft3.transfer, only that the balance is transfered to exchange_account with same account as the sender
function _transfer_to_exchange(inputs: list<ft3.xfer_input>, outputs: list<ft3.xfer_output>){
val sum_inputs = map<ft3.asset, integer>();
var idx = 0;
for (i in inputs) {
ft3.consume_input(i, idx, sum_inputs);
idx++;
}
idx = 0;
for(o in outputs) {
print(ft3.asset@{.id == o.asset_id}.name, o.amount);
process_exchange_output(o, idx, sum_inputs);
idx++;
}
}
operation transfer_to_exchange(inputs: list<ft3.xfer_input>, outputs: list<ft3.xfer_output>){
_transfer_to_exchange(inputs, outputs);
}
function transfer_from_exchange (inputs: list<ft3.xfer_input>, outputs: list<ft3.xfer_output>) {
val sum_inputs = map<ft3.asset, integer>();
var idx = 0;
for(i in inputs) {
consume_exchange_input(i, idx, sum_inputs);
idx++;
}
idx = 0;
for(o in outputs) {
ft3.process_transfer_output(o, idx, sum_inputs);
idx++;
}
}
operation withdraw_from_exchange(inputs: list<ft3.xfer_input>, outputs: list<ft3.xfer_output>){
transfer_from_exchange(inputs, outputs);
}
function dev_ensure_account(desc: ft3.auth_descriptor): ft3.account {
val is_account = ft3.account_auth_descriptor@?{.descriptor_id == desc.hash()};
return if (is_account != null) is_account.account else ft3.create_account_with_auth(desc);
}
query get_account_tradable_balance(account_id: byte_array) {
return tradable_balance@*{.account.id == account_id}(
name = .asset.name,
account_id = .account.id,
amount = .amount
);
}
query get_order_by_id(order_id: byte_array) = order@{.id == order_id}(
id = .id,
from_account_id = .account.id,
pair_from_id = .pair.from.id,
pair_to_id = .pair.to.id,
rate = .rate,
amount_from = .amount_from,
partial_fulfill = .partial_fulfill,
active = .active
);
query get_order_log_by_order_id(order_id: byte_array) {
return order@{.id == order_id}(
account_id = .order_log.account.id,
block_height = .order_log.block_height
);
}
|
Comments (0)
You can clone a snippet to your computer for local editing. Learn more.