├── README.md └── scripts └── exchange.rb /README.md: -------------------------------------------------------------------------------- 1 | # yoyow-core 2 | 3 | ## Docs 4 | * https://github.com/yoyow-org/yoyow-core/wiki 5 | 6 | ## More Info 7 | * https://yoyow.org/ 8 | * https://wallet.yoyow.org/ 9 | -------------------------------------------------------------------------------- /scripts/exchange.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'time' 4 | require 'bigdecimal' 5 | require "httpclient" 6 | require "json" 7 | require 'mysql2' 8 | require 'rufus-scheduler' 9 | 10 | $my_yoyow_check_config = { 11 | "head_age_threshold" => 15, #seconds 12 | "participation_rate_threshold" => 79.999 #percent 13 | } 14 | $my_exchange_config = { 15 | "yoyow_account" => "123456789", 16 | "yoyow_asset_id" => 0, 17 | "mysql_config" => { 18 | "host" => "192.168.1.1", 19 | "user" => "exchange", 20 | "pass" => "Yoyow12345", 21 | "db" => "exchange" 22 | } 23 | } 24 | def my_yoyow_config 25 | { 26 | "uri" => 'http://127.0.0.1:8091/rpc', 27 | "user" => "", 28 | "pass" => "" 29 | } 30 | end 31 | 32 | def yoyow_post (data:{}, timeout:55) 33 | if data.nil? or data.empty? 34 | return 35 | end 36 | 37 | client = HTTPClient.new 38 | client.connect_timeout=timeout 39 | client.receive_timeout=timeout 40 | 41 | myconfig = my_yoyow_config 42 | uri = myconfig["uri"] 43 | user = myconfig["user"] 44 | pass = myconfig["pass"] 45 | 46 | client.set_auth uri, user, pass 47 | 48 | begin 49 | response = client.post uri, data.to_json, nil 50 | response_content = response.content 51 | response_json = JSON.parse response_content 52 | return response_json 53 | rescue Exception => e 54 | print "yoyow_post error: " 55 | puts e 56 | end 57 | end 58 | 59 | # params is an array 60 | def yoyow_command (command:nil, params:nil, timeout:55) 61 | if command.nil? or params.nil? 62 | return 63 | end 64 | data = { 65 | "jsonrpc" => "2.0", 66 | "method" => command, 67 | "params" => params, 68 | "id" => 0 69 | } 70 | return yoyow_post data:data, timeout:timeout 71 | end 72 | 73 | #main 74 | if __FILE__ == $0 75 | 76 | logger = Logger.new(STDOUT) 77 | gwcf = $my_exchange_config 78 | sqlcf = gwcf["mysql_config"] 79 | sqlc = Mysql2::Client.new(:host=>sqlcf["host"],:username=>sqlcf["user"],:password=>sqlcf["pass"],:database=>sqlcf["db"]) 80 | yoyow_account = gwcf["yoyow_account"] 81 | yoyow_asset_id = gwcf["yoyow_asset_id"] 82 | 83 | # run 84 | scheduler = Rufus::Scheduler.new 85 | scheduler.every '10', first: :now, overlap: false do 86 | begin 87 | # [YOYOW] check if current node or current network status is ok 88 | r = yoyow_command command:"is_locked", params:[] 89 | if r["result"] == true 90 | msg = "[YOYOW] Warning: wallet is locked." 91 | logger.warn msg 92 | next 93 | end 94 | 95 | r = yoyow_command command:"info", params:[] 96 | yoyow_head_num = r["result"]["head_block_num"] 97 | yoyow_head_time = r["result"]["head_block_time"] 98 | yoyow_head_age_sec = (Time.now.utc - Time.parse(yoyow_head_time + 'Z')).to_i 99 | yoyow_lib = r["result"]["last_irreversible_block_num"] 100 | yoyow_participation_rate = BigDecimal.new(r["result"]["participation"]) 101 | 102 | logger.info "[YOYOW] head block #%d, LIB #%d, time %s, age %d s, NPR %.2f %" % 103 | [ yoyow_head_num, yoyow_lib, yoyow_head_time, yoyow_head_age_sec, yoyow_participation_rate.to_f ] 104 | if yoyow_head_age_sec >= $my_yoyow_check_config["head_age_threshold"] 105 | msg = "[YOYOW] Warning: head block age %d seconds." % yoyow_head_age_sec 106 | logger.warn msg 107 | next 108 | end 109 | if yoyow_participation_rate <= $my_yoyow_check_config["participation_rate_threshold"] 110 | msg = "[YOYOW] Warning: participation rate %.2f %" % yoyow_participation_rate.to_f 111 | logger.warn msg 112 | next 113 | end 114 | 115 | # [SQL] reconnect 116 | if sqlc.nil? or sqlc.closed? 117 | sqlc = Mysql2::Client.new(:host=>sqlcf["host"],:username=>sqlcf["user"],:password=>sqlcf["pass"],:database=>sqlcf["db"]) 118 | end 119 | 120 | # [YOYOW] get last processed number 121 | yoyow_next_seq = 1 122 | sql = "SELECT next_seq_no FROM yoyow_info WHERE monitor_account = " + yoyow_account 123 | sqlr = sqlc.query(sql) 124 | if sqlr.size == 0 125 | sqlc.query("insert into yoyow_info(monitor_account,next_seq_no) values(" + yoyow_account + ",1)") 126 | else 127 | sqlr.each do |row| 128 | # do something with row, it's ready to rock 129 | yoyow_next_seq = row["next_seq_no"] 130 | end 131 | end 132 | logger.info "[YOYOW] next seq to process = %d" % yoyow_next_seq 133 | 134 | # [YOYOW] process new history 135 | sql = "insert into yoyow_his(monitor_account,seq_no,from_account,to_account,amount,asset_id," + 136 | "decrypted_memo,description,block_num,block_time,trx_in_block,op_in_trx,virtual_op,trx_id," + 137 | "process_status) " + 138 | "values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" 139 | stmt = sqlc.prepare(sql) 140 | yoyow_last_block_num = 0 141 | yoyow_last_block = nil 142 | yoyow_max_seq = 0 143 | r = yoyow_command command:"get_relative_account_history", params:[yoyow_account,0,0,1,0] 144 | if not r["result"].empty? 145 | yoyow_max_seq = r["result"][0]["sequence"] 146 | end 147 | r = yoyow_command command:"get_relative_account_history", params:[yoyow_account,0,yoyow_next_seq,10,yoyow_next_seq+9] 148 | catch :reached_lib do 149 | while yoyow_next_seq <= yoyow_max_seq do 150 | r["result"].reverse.each{ |rec| 151 | seq_no = rec["sequence"] 152 | block_num = rec["op"]["block_num"] 153 | throw :reached_lib if block_num >= yoyow_lib # don't process records later than LIB 154 | yoyow_next_seq = seq_no + 1 155 | next if rec["op"]["op"][0] != 0 # not transfer 156 | logger.info "[YOYOW]" + rec.to_s 157 | sqlr = sqlc.query("select 1 from yoyow_his where monitor_account = " + yoyow_account + " and seq_no = " + seq_no.to_s) 158 | if sqlr.size > 0 159 | logger.info "[YOYOW] skipping because [account,seq] = [%s,%d] is already in db" % [ yoyow_account, seq_no ] 160 | next 161 | end 162 | if block_num != yoyow_last_block_num 163 | yoyow_last_block_num = block_num 164 | r1 = yoyow_command command:"get_block", params:[block_num] 165 | yoyow_last_block = r1["result"] 166 | end 167 | from_account = rec["op"]["op"][1]["from"].to_s 168 | to_account = rec["op"]["op"][1]["to"].to_s 169 | amount = rec["op"]["op"][1]["amount"]["amount"].to_i 170 | asset_id = rec["op"]["op"][1]["amount"]["asset_id"] 171 | decrypted_memo = rec["memo"] 172 | description = rec["description"] 173 | block_time = rec["op"]["block_timestamp"] 174 | trx_in_block = rec["op"]["trx_in_block"] 175 | op_in_trx = rec["op"]["op_in_trx"] 176 | virtual_op = rec["op"]["virtual_op"] 177 | trx_id = yoyow_last_block["transaction_ids"][trx_in_block] 178 | process_status = 0 # new 179 | if from_account == yoyow_account 180 | process_status = 2 # out 181 | sqlc.query("update withdraw_his set process_status = 23, out_block_num = " + block_num.to_s + 182 | " where out_platform = 'yoyow' " + 183 | "and out_trx_id = '" + trx_id + "' and process_status = 22") 184 | elsif asset_id != yoyow_asset_id 185 | process_status = 101 # bad asset 186 | elsif decrypted_memo.nil? or decrypted_memo.length == 0 187 | process_status = 102 # empty memo 188 | else 189 | # parse memo 190 | memo_to_parse = decrypted_memo 191 | # check if the memo is valid 192 | if memo_to_parse.ok? 193 | process_status = 11 # good memo 194 | # to process 195 | else 196 | process_status = 104 # bad memo 197 | end 198 | end 199 | stmt.execute(yoyow_account,seq_no,from_account.to_i,to_account.to_i,amount,asset_id, 200 | decrypted_memo,description,block_num,block_time,trx_in_block,op_in_trx,virtual_op,trx_id, 201 | process_status) 202 | } 203 | r = yoyow_command command:"get_relative_account_history", params:[yoyow_account,0,yoyow_next_seq,10,yoyow_next_seq+9] 204 | end # while 205 | end #catch 206 | sqlc.query("update yoyow_info set next_seq_no = " + yoyow_next_seq.to_s + " where monitor_account = " + yoyow_account) 207 | 208 | # [SQL] process new withdrawals 209 | sql = "select * from withdraw_his where process_status = 11 order by seq_no" 210 | sqlr = sqlc.query(sql) 211 | if sqlr.size > 0 212 | catch :collected_csaf do 213 | # [YOYOW] check account statistics 214 | r = yoyow_command command:"get_full_account", params:[yoyow_account] 215 | stats = r["result"]["statistics"] 216 | if stats["csaf"] < 2000000 # 20 YOYO 217 | logger.info "[YOYOW] collecting CSAF" 218 | r1 = yoyow_command command:"collect_csaf", params:[yoyow_account,yoyow_account,20,"YOYO",true] 219 | logger.info r1.to_s 220 | if not r1["error"].nil? 221 | msg = "[YOYOW] Warning: insufficient CSAF" 222 | logger.warn msg 223 | end 224 | throw :collected_csaf # do nothing until next loop 225 | end 226 | # [YOYOW] process 227 | avail_balance = BigDecimal.new(stats["core_balance"]) - 228 | BigDecimal.new(stats["total_witness_pledge"]) - 229 | BigDecimal.new(stats["total_committee_member_pledge"]) 230 | sqlr.each do |row| 231 | out_amount = row["out_amount"] # raw data, should / 100000 232 | if avail_balance >= out_amount 233 | row_id = row["row_id"] 234 | out_addr = row["out_address"] 235 | out_memo = row["out_memo"] 236 | float_out_amount_str = "%d.%05d" % [ out_amount / 100000, out_amount % 100000 ] 237 | logger.info "[SQL] ready to process: -> YOYOW(%s), %s YOYO" % 238 | [ out_addr, float_out_amount_str ] 239 | sqlc.query("update withdraw_his set process_status = 21 where row_id = " + row_id.to_s) 240 | logger.info "[YOYOW] sending..." 241 | out_time = Time.now.utc.to_s 242 | r1 = yoyow_command command:"transfer", params:[yoyow_account,out_addr,float_out_amount_str,"YOYO",out_memo,true] 243 | logger.info r1.to_s 244 | if not r1["error"].nil? # fail 245 | logger.warn "[YOYOW] error while sending" 246 | sql1 = "update withdraw_his set process_status = 201, out_time = ?, out_detail = ? where row_id = ?" 247 | stmt1 = sqlc.prepare(sql1) 248 | stmt1.execute(out_time, r1["error"].to_s, row_id) 249 | elsif not r1["result"].nil? # ok 250 | logger.info "[YOYOW] done sending" 251 | trx = r1["result"] 252 | r2 = yoyow_command command:"get_transaction_id", params:[trx] 253 | trx_id = r2["result"] 254 | sql1 = "update withdraw_his set process_status = 22, out_time = ?, out_trx_id = ?, out_detail = ? where row_id = ?" 255 | stmt1 = sqlc.prepare(sql1) 256 | stmt1.execute(out_time, trx_id, trx.to_s, row_id) 257 | else # should not be here, just to be safe 258 | sql1 = "update withdraw_his set process_status = 202, out_time = ?, out_detail = ? where row_id = ?" 259 | stmt1 = sqlc.prepare(sql1) 260 | stmt1.execute(out_time, r1.to_s, row_id) 261 | end 262 | break # process one item every (outer) loop 263 | else 264 | msg = "[YOYOW] Warning: insufficient balance" 265 | logger.warn msg 266 | end # if avail_balance 267 | end # sqlr.each 268 | end # catch CSAF 269 | end # if sqlr.size > 0 270 | rescue Exception => e 271 | logger.error e.to_s 272 | logger.error e.backtrace 273 | msg = "Exception: %s" % e.to_s 274 | end 275 | end 276 | 277 | scheduler.join 278 | 279 | end 280 | __END__ 281 | --------------------------------------------------------------------------------