Удивительно, как мало готовых (целевых) решений для работы с крипто-кошельками на серверной стороне. А конкретно для таких базовых задач, как: отправка самой монеты, отправка токенов, получение баланса, подпись транзакций.

Опытные - ничего нового в этой статье не найдут, но она будет полезна тем, кто ради базовых функций не хочет штудировать документацию 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');

Комментарии (3)


  1. x88
    12.01.2024 21:44
    +3

    Пакет и статья, скорее вредные, чем полезные:
    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.


    1. Kelbon
      12.01.2024 21:44
      -2

      То есть писать что то связанное с деньгами на жс это не смущает?)


      1. trueMoRoZ
        12.01.2024 21:44
        +2

        Не смущает. У вас есть какие-то убеждения на этот счёт?