Удивительно, как мало готовых (целевых) решений для работы с крипто-кошельками на серверной стороне. А конкретно для таких базовых задач, как: отправка самой монеты, отправка токенов, получение баланса, подпись транзакций.
Опытные - ничего нового в этой статье не найдут, но она будет полезна тем, кто ради базовых функций не хочет штудировать документацию tronweb, web3 и прочих библиотек для работы с криптомиром. По сути - я просто хочу собрать в одном месте полезные кусочки кода.
Если кто-то ищет готовое решение, то я упаковал все в NPM-пакет.
npm i encode-mnemonic-wallet
Tron (TRX)
Для работы с Tron нам потребуется официальная библиотека tronweb. Также потребуется рабочая Tron-нода, мы будем использовать бесплатную, доступную для всех trongrid.
let TronWeb = require('tronweb');
let HttpProvider = TronWeb.providers.HttpProvider;
let fullNode = new HttpProvider("https://api.trongrid.io");
let solidityNode = new HttpProvider("https://api.trongrid.io");
let eventServer = new HttpProvider("https://api.trongrid.io");
let privateKey = "приватный ключ вашего кошелька";
let wallet = new TronWeb(fullNode, solidityNode, eventServer, privateKey);
let address = TronWeb.address.fromPrivateKey(privateKey);
Подпись транзакций
Вообще - можно миновать этот этап. TronWeb из коробки дает упрощенные функции, которые позволяют сразу и подписать и отправить монеты. Но я думаю, для некоторых это будет полезная инфорация - поэтому дальнейший код будет строиться на отдельной подписи и дальнейшей отправке транзакций.
async function signTransaction(transaction){
return await wallet.trx.sign(transaction, privateKey);
}
Отправка монет TRX
async function send(amount, toAddress){
//Создаем траназкцию
let transaction = await wallet.transactionBuilder.sendTrx(toAddress, amount * 1000000);
//Подписываем и отправляем транзакцию
let response = await wallet.trx.sendRawTransaction(
await signTransaction(transaction.transaction)
);
return response.txid;
}
Получение баланса TRX
async function getBalance(address){
let balance = await wallet.trx.getBalance(address);
return balance / 1000000;
}
Работа с токенами TRC20
Для работы с каким-либо токеном, нам нужно иметь под рукой данные нужного токена, а именно адрес и ABI.
//Можно заготовить данные заранее
let USDT = {
address : 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
abi : [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_upgradedAddress","type":"address"}],"name":"deprecate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"deprecated","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_evilUser","type":"address"}],"name":"addBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"upgradedAddress","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"maximumFee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_maker","type":"address"}],"name":"getBlackListStatus","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseApproval","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_value","type":"uint256"}],"name":"calcFee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"oldBalanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newBasisPoints","type":"uint256"},{"name":"newMaxFee","type":"uint256"}],"name":"setParams","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"issue","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseApproval","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"redeem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"basisPointsRate","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isBlackListed","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_clearedUser","type":"address"}],"name":"removeBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MAX_UINT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_blackListedUser","type":"address"}],"name":"destroyBlackFunds","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_initialSupply","type":"uint256"},{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_decimals","type":"uint8"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_blackListedUser","type":"address"},{"indexed":false,"name":"_balance","type":"uint256"}],"name":"DestroyedBlackFunds","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Issue","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Redeem","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newAddress","type":"address"}],"name":"Deprecate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_user","type":"address"}],"name":"AddedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_user","type":"address"}],"name":"RemovedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"feeBasisPoints","type":"uint256"},{"indexed":false,"name":"maxFee","type":"uint256"}],"name":"Params","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]
}
//Либо, как вариант - ABI можно получить на лету
let {abi} = await wallet.trx.getContract("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t");
USDT.abi = abi;
Отправка токенов TRC20
async function sendToken(contract, amount, toAddress){
//Создание транзакции
let transaction = await wallet.transactionBuilder.triggerSmartContract(
contract.address,
'transfer(address,uint256)',
{},
[
{
type : 'address',
value : toAddress
},
{
type : 'uint256',
value : amount
}
],
toAddress
);
//Точно так же подписываем и отправляем транзакцию
let response = await wallet.trx.sendRawTransaction(
await signTransaction(transaction.transaction)
);
return response.txid;
}
Получение баланса токена TRC20
async function getBalanceToken(contract, address){
let contractWrap = await this.tronWeb.contract(contract.abi, contract.address);
let balance = await contractWrap.methods.balanceOf(address).call();
return +balance.toString();
}
ETH, BSC
Для работы с этими ребятами будем использовать библиотеку web3. В целом код будет одинаковый, так как технически BSC это форк ETH. Отличаться будет только адрес ноды.
let Web3 = require("web3");
//Для ETH можно использовать публичную ноду mycryptoapi
let wallet = new Web3('https://api.mycryptoapi.com/eth');
//Для BSC можно использовать публичную ноду binance
let wallet = new Web3('https://bsc-dataseed.binance.org/');
let privateKey = "приватный ключ вашего кошелька";
let address = wallet.eth.accounts.privateKeyToAccount(privateKey).address;
Подпись транзакций
async function signTransaction(){
let gas = await wallet.eth.estimateGas(transaction);
return await wallet.eth.accounts.signTransaction(
{
gas,
...transaction
},
privateKey
);
}
Отправка монет ETH, BSC
async function send(amount, toAddress){
//Создание транзакции
let transaction = {
to : toAddress,
value : amount * 1000000000000000000
}
//Подпись и отправка транзакции
let response = await wallet.eth.sendSignedTransaction(
await signTransaction(transaction).rawTransaction
);
return response.transactionHash;
}
Получение баланса ETH, BSC
async function getBalance(address){
let balance = await wallet.eth.getBalance(address);
return balance / 1000000000000000000;
}
Работа с токенами
Опять условимся, что мы заранее заготовили данные:
let USDT = {
address : '0xdac17f958d2ee523a2206206994597c13d831ec7',
abi : [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_upgradedAddress","type":"address"}],"name":"deprecate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"deprecated","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_evilUser","type":"address"}],"name":"addBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"upgradedAddress","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balances","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"maximumFee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_maker","type":"address"}],"name":"getBlackListStatus","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowed","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getOwner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newBasisPoints","type":"uint256"},{"name":"newMaxFee","type":"uint256"}],"name":"setParams","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"issue","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"redeem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"basisPointsRate","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isBlackListed","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_clearedUser","type":"address"}],"name":"removeBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MAX_UINT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_blackListedUser","type":"address"}],"name":"destroyBlackFunds","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_initialSupply","type":"uint256"},{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_decimals","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Issue","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Redeem","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newAddress","type":"address"}],"name":"Deprecate","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"feeBasisPoints","type":"uint256"},{"indexed":false,"name":"maxFee","type":"uint256"}],"name":"Params","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_blackListedUser","type":"address"},{"indexed":false,"name":"_balance","type":"uint256"}],"name":"DestroyedBlackFunds","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_user","type":"address"}],"name":"AddedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_user","type":"address"}],"name":"RemovedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"}]
}
Отправка токенов BEP20, ERC20
async function sendToken(contract, amount, toAddress){
let contractWrap = await wallet.eth.Contract(contract.abi, contract.address);
//Создание транзакции
let transfer = contractWrap.methods.transfer(toAddress, amount);
let transaction = {
from : address,
to : contract.address,
data: transfer.encodeABI(),
}
//Подпись и отправка
let response = await wallet.eth.sendSignedTransaction(
await signTransaction(transaction).rawTransaction
);
return response.transactionHash;
}
Получение баланса токенов ERC20, BEP20
async function getBalanceToken(contract, address){
let contractWrap = await wallet.eth.Contract(contract.abi, contract.address);
let balance = await contractWrap.methods.balanceOf(address).call();
return +balance;
}
Генерация мнемоники, сида, приват ключей
Бонусом оставлю здесь информацию о генерации кошельков
let Bip32 = require('bip32')(require('tiny-secp256k1'));
let Bip39 = require("bip39");
//Генерация мнемоники
let mnemonic = Bip39.generateMnemonic();
//Получение seed из мнемоники
let seed = (await Bip39.mnemonicToSeed(mnemonic)).toString('hex');
//Получение приватного ключа из seed
let node = await Bip32
.fromSeed(Buffer.from(seed, 'hex'))
.derivePath(`m/44'/195'/0'/0/0`);
let privateKey = node.privateKey.toString('hex');
x88
Пакет и статья, скорее вредные, чем полезные:
1. Неверная работа с big numeric ( просто деление не прокатит), а также в Tron и EVM сетях разные юниты данных (10^6 и 10^18) соответственно
2. Во всех приведенных сетях формирование транзакций и подсчет газа отличается, а не "только адрес" (к стати в Tron представление адреса может быть и в HEX формате). В BSC до используются legacy транзакции, когда как в EVM после EIP-1559 - динамические, а Tron считается условно-совместимой EVM сетью, и там цена на газ и транзакции формируются иначе.
3. Вы используете для формирования пары ключей BIP-44 деривативный адрес для сети TRX, без описания (ну и сохранения мнемоники, указания энтропии passphrase, которая, к примеру используется при эталонных векторах тестирования).
4. Нет информации про кусок вашего ABI (причем для одного метода).
Примеры также не информативны и "вредны". В таком виде не подает информацию даже ChatGPT.
Kelbon
То есть писать что то связанное с деньгами на жс это не смущает?)
trueMoRoZ
Не смущает. У вас есть какие-то убеждения на этот счёт?