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)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.