Для написания скриптов для работы с блокчейном Голос можно использовать готовые рабочие библиотеки на javascript либо python, которые описаны в wiki Голоса. Те, кого по каким-либо причинам они не устраивают (и там и там есть "ньюансы", но они выходят за рамки данного поста), могут обращаться к нодам напрямую вызывая API функции. Ниже я хочу дать пример рабочего скрипта, который стоит у меня на сервисе обмена Tip-баланса на ликвидные голоса. Из библиотеки golos-js используется только функция подписи транзакции. Может быть кому-то это и пригодится как основа для своей поделки.
// подключаем необходимые библиотеки
const ping = require('ping');
const lockFile = require('lockfile');
const MySql = require('sync-mysql');
const request = require('sync-request');
const golos = require('golos-classic-js');
// глобальные параметры
// список доступных публичных API нод GOLOS
const golos_api_nodes = ['api.aleksw.space', 'golos.lexai.host', 'golos.viz.media', 'api-golos.blckchnd.com'];
// путь для lock файла
const path = '/home/igor/golos/tip2golos_cron.lock';
const opts = {};
// явки/пароли для локальной mysql
const db_host = 'localhost';
const db_name = 'golos';
const db_user = 'golos';
const db_passwd = 'xxxxxxxx';
// параметры для голосового аккаунта
const account = 'ecurrex-t2g';
const from = -1;
const limit = 100;
const wif_active = '5...';
const wif_posting = '5...';
// ============================================================================
async function main()
{
// 1 шаг
// проверяем доступность нод(ы) для работы
for(let host of golos_api_nodes){
let node = await ping.promise.probe(host);
// находим первую живую ноду
if (node.alive) {
console.log('начинаем выполнять скрипт');
// 2 шаг
// проверяем на lock файл от прошлого запуска
let isLocked = lockFile.checkSync(path, [opts]);
// если прошлого лока нет
if (!isLocked) {
// создаем текущий lock файл
lockFile.lockSync(path, [opts]);
console.log('locked!');
// 3 шаг
// подключаем локальную базу данных
console.log('open database');
mysql = new MySql({
host: db_host,
user: db_user,
database: db_name,
password: db_passwd
});
// 4 шаг
// получаем историю по аккаунту
let history = get_history(host, account, from, limit);
// в цикле прогоняем все полученные записи
// в количестве limit либо сколько есть
let i = 0;
while (history[i] != undefined) {
let j = 0;
while (history[i][1].op[j] != undefined) {
// отбираем только тип операции - 'donate'
if (history[i][1].op[j] == 'donate') {
// 5 шаг
// разбор истории по транзакциям
// номер блока
let blocknum = history[i][1].block;
// номер транзакции
let trx_id = history[i][1].trx_id;
// от кого донат
let acc_from = history[i][1].op[1].from;
// к кому донат
let acc_to = history[i][1].op[1].to;
// разбираем донат на сумму и что за токен
let amount = history[i][1].op[1].amount;
let amountArray = amount.split(" ");
// получаем саму сумму
let summa = amountArray[0];
// получаем что за токен
let token = amountArray[1];
// если наш аккаунт получил GOLOS
if (token == 'GOLOS' && acc_from != account) {
console.log('запрос обработанной транзакции');
// проверяем, не обработали ли уже ее раньше
let sql = "select count(*) as count from golos_history where blocknum = " +
blocknum + " and trx_id = '" + trx_id + "' and acc_from = '" + acc_from + "'";
let db_result = mysql.query(sql);
let count = db_result[0].count;
// если такой донат еще не обрабатывали
if (count == 0) {
console.log('заносим в таблицу транзакцию как обработанную');
let curr_date = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
let sql = "insert into golos_history (blocknum, trx_id, acc_from, amount, out_address, processed) values (" +
blocknum + ",'" + trx_id + "','" + acc_from + "','" + amount + "','" + acc_from + "','" + curr_date + "')";
let db_result = mysql.query(sql);
// 6 шаг
// производим обмен или делаем возврат
// получаем баланс нашего аккаунта
let acc_balance = get_liquid_balance(host, account);
// если ликвидных голосов больше чем запрошенная сумма на обмен
if (parseFloat(acc_balance) >= parseFloat(summa)) {
console.log('обмен');
summa = summa * 0.95;
let amount_str = summa.toFixed(3) + ' ' + 'GOLOS';
let memo = 'обмен tip на golos';
let resp = transfer(host, wif_active, account, acc_from, amount_str, memo);
if (resp != undefined) { console.log(resp); }
else { console.log('transaction is undefined'); } // тут можно откатить из базы обработку, чтобы попробовать в след. раз
}
// иначе делаем возврат
else {
console.log('возврат');
let amount_str = summa + ' ' + 'GOLOS';
let resp = donate(host, wif_posting, account, acc_from, amount_str);
if (resp != undefined) { console.log(resp); }
else { console.log('transaction is undefined'); } // тут можно откатить из базы обработку, чтобы попробовать в след. раз
}
}
else {
console.log('уже обработано');
}
}
}
j++;
}
i++;
}
// закрываем соединение с базой данных
console.log('close database')
mysql.dispose()
// удаляем за собой lock файл
console.log('unlocked!');
lockFile.unlockSync(path);
}
// и выходим из цикла
break;
}
else { console.log('сеть/нода(ы) упала. скрипт не работает'); }
}
}
// ==================================================================
function get_head_block_num (host) {
var res = request('POST', 'https://' + host + '/', {
headers: {'content-type': 'text/plain' },
body: '{"id":1,"method":"call","jsonrpc":"2.0","params":["database_api","get_dynamic_global_properties",[] ]}'
});
var chunk = res.body;
var textChunk = chunk.toString('utf8');
var json = JSON.parse(textChunk);
var properties = json.result;
return (properties.head_block_number);
}
// ==================================================================
function get_block_header(host, param) {
var res = request('POST', 'https://' + host + '/', {
headers: {'content-type': 'text/plain' },
body: '{"id":1,"method":"call","jsonrpc":"2.0","params":["database_api","get_block_header",["' + param + '"] ]}'
});
var chunk = res.body;
var textChunk = chunk.toString('utf8');
var json = JSON.parse(textChunk);
var block_header = json.result;
return (block_header);
}
// ==================================================================
function transfer(host, wif, account, golos_acc, amount_str, memo) {
// строим транзакцию
let head_block_number = get_head_block_num(host);
// ref_block_num
let ref_block_num = (head_block_number - 3) & 0xffff;
// ref_block_prefix
let var1 = head_block_number - 2;
block_header = get_block_header(host, var1);
let ref_block_prefix = new Buffer(block_header.previous, 'hex').readUInt32LE(4)
// expiration
const now = new Date().getTime() + 120 * 1000;
const expiration = new Date(now).toISOString().split('.')[0]
let ops = [];
ops.push(["transfer",
{
'from' : account,
'to' : golos_acc,
'amount' : amount_str,
'memo' : memo
}
]);
const unsigned_trx = {
'expiration': expiration,
'extensions': [],
'operations': ops,
'ref_block_num': ref_block_num,
'ref_block_prefix': ref_block_prefix
}
let signed_trx = null;
try {
signed_trx = golos.auth.signTransaction(unsigned_trx,{"active":wif})
}
catch (error) {
console.warn("Не удалось подписать транзакцию: " + error.message)
}
var res = request('POST', 'https://' + host + '/', {
headers: {'content-type': 'text/plain' },
body: '{"id":1,"method":"call","jsonrpc":"2.0","params":["network_broadcast_api","broadcast_transaction_synchronous",[{"ref_block_num":' + ref_block_num +',"ref_block_prefix":'+ref_block_prefix+',"expiration":"'+expiration+'","operations":[["transfer",{"from":"'+account+'","to":"'+golos_acc+'","amount":"'+amount_str+'","memo":"'+memo+'"}]],"extensions":[],"signatures":["'+signed_trx.signatures+'"]} ]]}'
});
var chunk = res.body;
var textChunk = chunk.toString('utf8');
var json = JSON.parse(textChunk);
var transaction = json.result;
return (transaction.id);
}
// ==================================================================
function donate(host, wif, account, golos_acc, amount_str) {
// строим транзакцию
let head_block_number = get_head_block_num(host);
// ref_block_num
let ref_block_num = (head_block_number - 3) & 0xffff;
// ref_block_prefix
let var1 = head_block_number - 2;
block_header = get_block_header(host, var1);
let ref_block_prefix = new Buffer(block_header.previous, 'hex').readUInt32LE(4)
// expiration
const now = new Date().getTime() + 120 * 1000;
const expiration = new Date(now).toISOString().split('.')[0]
let donate_memo = {"app":"tip2golos", "version":1, "target": {"author": "ecurrex-t2g", "permlink": ""}, "comment":"возврат. запрошенная сумма больше резерва"};
var memo2str = JSON.stringify(donate_memo);
let ops = [];
ops.push(["donate",
{
'from' : account,
'to' : golos_acc,
'amount' : amount_str,
'memo' : donate_memo
}
]);
const unsigned_trx = {
'expiration': expiration,
'extensions': [],
'operations': ops,
'ref_block_num': ref_block_num,
'ref_block_prefix': ref_block_prefix
}
let signed_trx = null;
try {
signed_trx = golos.auth.signTransaction(unsigned_trx,{"posting":wif})
}
catch (error) {
console.warn("Не удалось подписать транзакцию: " + error.message)
}
var res = request('POST', 'https://' + host + '/', {
headers: {'content-type': 'text/plain' },
body: '{"id":1,"method":"call","jsonrpc":"2.0","params":["network_broadcast_api","broadcast_transaction_synchronous",[{"ref_block_num":' + ref_block_num +',"ref_block_prefix":'+ref_block_prefix+',"expiration":"'+expiration+'","operations":[["donate",{"from":"'+account+'","to":"'+golos_acc+'","amount":"'+amount_str+'","memo":'+memo2str+'}]],"extensions":[],"signatures":["'+signed_trx.signatures+'"]} ]]}'
});
var chunk = res.body;
var textChunk = chunk.toString('utf8');
var json = JSON.parse(textChunk);
var transaction = json.result;
return (transaction.id);
}
// ==================================================================
function get_history(host, account, from, limit) {
let res = request('POST', 'https://'+host+'/', {
headers: {'content-type': 'text/plain' },
body: '{"id":1,"method":"call","jsonrpc":"2.0","params":["account_history","get_account_history",["'+account+'", "'+from+'", "'+limit+'"] ]}'
});
let chunk = res.body;
let textChunk = chunk.toString('utf8');
let json = JSON.parse(textChunk);
let history = json.result;
return history;
}
// ==================================================================
function get_liquid_balance(host, account) {
let res = request('POST', 'https://'+host+'/', {
headers: {'content-type': 'text/plain' },
body: '{"id":1,"method":"call","jsonrpc":"2.0","params":["database_api","get_accounts",[["'+account+'"]]]}'
});
let chunk = res.body;
let textChunk = chunk.toString('utf8');
let json = JSON.parse(textChunk);
let result = json.result;
let amount = result[0].balance;
let amountArray = amount.split(" ");
let acc_balance = amountArray[0];
return acc_balance;
}
// ==================================================================
main();
@ecurrex-ru, отлично, спасибо что запубличил 👍️
@lex, ну это так, один из возможных вариантов (пример на коленке) , а не истина в последней инстанции, как правльно писать шлюзы. просто чтобы было немного понятнее, как оно там "изнутри" бывает. (вдруг это действительно кому-то понадобится или интересно). а то как формировать транзу руками нигде нет. я про "текст", а не ковыряние кода библиотек :) те. обрывки есть то там, то там. но вот целиком - взять 1-2-3 и получить итог, его подписать и отправить в бч - я не нашел.
@ecurrex-ru, Думаю многим будет полезно. Репостнул, апнул и задонатил :-)
@denis-skripnik, спасибо :)
Во благо.