¿Cómo enviar el token ERC20 usando la API Web3?

Creé un token personalizado en Ropsten testnet usando esta guía: https://steemit.com/ethereum/@maxnachamkin/how-to-create-your-own-ethereum-token-in-an-hour-erc20-verified

Puedo enviarlo a otras cuentas usando MetaMask, pero no sé cómo hacerlo en node.js usando web3, ethereumjs-tx y Web3 JavaScript app API.

Mi código en este momento se ve así:

var count = web3.eth.getTransactionCount("0x26...");
var abiArray = JSON.parse(fs.readFileSync('mycoin.json', 'utf-8'));
var contractAddress = "0x8...";
var contract = web3.eth.contract(abiArray).at(contractAddress);
var rawTransaction = {
    "from": "0x26...",
    "nonce": web3.toHex(count),
    "gasPrice": "0x04e3b29200",
    "gasLimit": "0x7458",
    "to": contractAddress,
    "value": "0x0",
    "data": contract.transfer("0xCb...", 10, {from: "0x26..."}),
    "chainId": 0x03
};

var privKey = new Buffer('fc3...', 'hex');
var tx = new Tx(rawTransaction);

tx.sign(privKey);
var serializedTx = tx.serialize();

web3.eth.sendRawTransaction('0x' + serializedTx.toString('hex'), function(err, hash) {
    if (!err)
        console.log(hash);
    else
        console.log(err);
});

En este caso, el código solo se detiene en una contract.transfer("0xCb...", 10, {from: "0x26..."})parte y la solicitud está pendiente. No se pudo encontrar ninguna guía para hacer cosas similares. Encontré algo de código aquí:

Web3 enviando tokens personalizados usando la función de transferencia. Necesidad de configurar la cuenta de

Y aquí:

Cómo transferir tokens ERC20 usando web3js

Pero todavía me quedé atascado, no sé lo que me estoy perdiendo.

Bueno, mirando la documentación de web3, puede probar contract.methods. transfer("0xCb...", 10).send({from: XX},function(){dostuff})en lugar de enviar sendRawTransaction.
Debe usar getDatapara generar los datos de transacción sin procesar, como este "data": contract.transfer.getData("0xCb...", 10, {from: "0x26..."}),.
Esta respuesta explica cómo usar getData ethereum.stackexchange.com/a/12932
@TomasNavickas ¿usabas web3.js v0.20.x? ¿Puede publicar un ejemplo completo como respuesta y marcarlo como resuelto? Gracias.
@TomasNavickas Esto funciona, pero está enviando wei (éter) en lugar del token real. ¿Alguna idea de por qué?
@Viper Wei (éter) se usa para pagar la tarifa de transacción. ¿Puede proporcionar algún ejemplo de transacción en el que podamos ver qué datos de entrada está enviando?
@TomasNavickas Lo agregué como una nueva pregunta aquí, ¡gracias! ethereum.stackexchange.com/questions/29513/…
¿Qué hace el Buffer en este caso? ¿Por qué la var privKey no se asigna simplemente como una cadena que contiene la clave privada?

Respuestas (6)

Estoy usando la versión web3.js: 0.20.1 en la aplicación express node.js. Estoy ejecutando Parity en la máquina Virtualbox Ubuntu.

El código correcto se parece a lo siguiente:

var count = web3.eth.getTransactionCount("0x26...");
var abiArray = JSON.parse(fs.readFileSync('mycoin.json', 'utf-8'));
var contractAddress = "0x8...";
var contract = web3.eth.contract(abiArray).at(contractAddress);
var rawTransaction = {
    "from": "0x26...",
    "nonce": web3.toHex(count),
    "gasPrice": "0x04e3b29200",
    "gasLimit": "0x7458",
    "to": contractAddress,
    "value": "0x0",
    "data": contract.transfer.getData("0xCb...", 10, {from: "0x26..."}),
    "chainId": 0x03
};

var privKey = new Buffer('fc3...', 'hex');
var tx = new Tx(rawTransaction);

tx.sign(privKey);
var serializedTx = tx.serialize();

web3.eth.sendRawTransaction('0x' + serializedTx.toString('hex'), function(err, hash) {
    if (!err)
        console.log(hash);
    else
        console.log(err);
});
¿Puede compartir el tutorial sobre el uso de web3.js para transferencias de éter y token?
¿Qué hace el Buffer en este caso? ¿Por qué la var privKey no se asigna simplemente como una cadena que contiene la clave privada?
¿Me imagino que se puede usar un ABI IERC20 genérico?

Encontré muchas respuestas que estaban desactualizadas o les faltaba información importante. Esto es lo que finalmente funcionó para mí en mi proyecto node.js usando web3 versión 1.0.0-beta.26. Tenga en cuenta que esto es para Ethereum Main Net. Para usar la red de prueba Robsten, cambie el chainId a 0x03

// Get private stuff from my .env file
import {my_privkey, infura_api_key} from '../.env'

// Need access to my path and file system
import path from 'path'
var fs = require('fs');

// Ethereum javascript libraries needed
import Web3 from 'Web3'
var Tx = require('ethereumjs-tx');

// Rather than using a local copy of geth, interact with the ethereum blockchain via infura.io
const web3 = new Web3(Web3.givenProvider || `https://mainnet.infura.io/` + infura_api_key)

// Create an async function so I can use the "await" keyword to wait for things to finish
const main = async () => {
  // This code was written and tested using web3 version 1.0.0-beta.26
  console.log(`web3 version: ${web3.version}`)

  // Who holds the token now?
  var myAddress = "0x97...";

  // Who are we trying to send this token to?
  var destAddress = "0x4f...";

  // If your token is divisible to 8 decimal places, 42 = 0.00000042 of your token
  var transferAmount = 1;

  // Determine the nonce
  var count = await web3.eth.getTransactionCount(myAddress);
  console.log(`num transactions so far: ${count}`);

  // This file is just JSON stolen from the contract page on etherscan.io under "Contract ABI"
  var abiArray = JSON.parse(fs.readFileSync(path.resolve(__dirname, './tt3.json'), 'utf-8'));

  // This is the address of the contract which created the ERC20 token
  var contractAddress = "0xe6...";
  var contract = new web3.eth.Contract(abiArray, contractAddress, { from: myAddress });

  // How many tokens do I have before sending?
  var balance = await contract.methods.balanceOf(myAddress).call();
  console.log(`Balance before send: ${balance}`);

  // I chose gas price and gas limit based on what ethereum wallet was recommending for a similar transaction. You may need to change the gas price!
  var rawTransaction = {
      "from": myAddress,
      "nonce": "0x" + count.toString(16),
      "gasPrice": "0x003B9ACA00",
      "gasLimit": "0x250CA",
      "to": contractAddress,
      "value": "0x0",
      "data": contract.methods.transfer(destAddress, transferAmount).encodeABI(),
      "chainId": 0x01
  };

  // Example private key (do not use): 'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109'
  // The private key must be for myAddress
  var privKey = new Buffer(my_privkey, 'hex');
  var tx = new Tx(rawTransaction);
  tx.sign(privKey);
  var serializedTx = tx.serialize();

  // Comment out these three lines if you don't really want to send the TX right now
  console.log(`Attempting to send signed tx:  ${serializedTx.toString('hex')}`);
  var receipt = await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'));
  console.log(`Receipt info:  ${JSON.stringify(receipt, null, '\t')}`);

  // The balance may not be updated yet, but let's check
  balance = await contract.methods.balanceOf(myAddress).call();
  console.log(`Balance after send: ${balance}`);
}

main();

Tenga en cuenta que a veces el envío "fallará" porque el tx no se extrajo dentro de los 50 bloques. En mi copia del código fuente de web3, cambié TIMEOUTBLOCK de 50 a 500 para no tener que lidiar con eso.

Tenga en cuenta que este código no tiene manejo de errores; es posible que desee agregar algunos.

Entonces, ¿uno necesitaría obtener y administrar el abi json para cada ERC-20 individual que deba manejarse?
¿O es posible usar un 'ABI de token ERC20 estándar' como sugiere este módulo? github.com/danfinlay/human-standard-token-abi
Para llamar a una función en cualquier contrato, todo lo que necesita es la dirección del contrato y la firma de la función. ERC20 define varias funciones. Siempre que el contrato en la dirección defina una función pública con la misma firma que la que está llamando, puede llamarla.
Recibo este error: Error: Error devuelto: remitente no válido
@dacoinminster, si envío ether, entonces funciona, pero si envío el token, aparece un error Error: error devuelto: remitente no válido ¿Estoy haciendo algo mal?
Para testnet (ropsnet) use chainId: hex(3)
¿puedes usar un número aleatorio para el nonce?
@zero_cool no, tiene que ser más grande que el nonce anterior. Por eso llamamos getTransactionCount.
Recibo una información de recibo vacía y mis tokens no se transfieren. Recibí el error "La transacción no se extrajo en 750 segundos, asegúrese de que su transacción se envió correctamente. ¡Tenga en cuenta que aún podría ser extraída!" después de 15 minutos que hacer?
¿Qué hace el Buffer en este caso? ¿Por qué la var privKey no se asigna simplemente como una cadena que contiene la clave privada?

Si solo queremos enviar una transacción con el método erc20 transfer, podemos construir un objeto de contrato usando minABI y la dirección del contrato, como el siguiente código:

let minABI = [
// transfer
{
    "constant": false,
    "inputs": [
        {
            "name": "_to",
            "type": "address"
        },
        {
            "name": "_value",
            "type": "uint256"
        }
    ],
    "name": "transfer",
    "outputs": [
        {
            "name": "success",
            "type": "bool"
        }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
}
];
let contractAddres="put the erc20 contract address here";

let contract = await new web3.eth.Contract(minABI, contractAddr);

contract.methods.transfer("your account", "amount of erc20 tokens you want transfer").send({
        from: "your account"
    });

y esto es trabajo para mí, espero que esto ayude. :D

Gracias, esto es mejor que las otras respuestas, no necesita y generalmente no tiene el ABI de un token ERC20 público.

Puede verificar este ejemplo de trabajo con mi token: https://github.com/religion-counter/onlyone/blob/main/helper-scripts/send-onlyone.js

También puede contribuir al repositorio si está interesado.

// Helper script that sends ONLYONE token to target addresses specified in targets.txt
// Target index - index in targets.txt file is specified as an argument - process.argv.splice(2)[0]

var fs = require('fs')

var targetAccounts = JSON.parse(fs.readFileSync('targets.txt', 'utf-8'));

var myAddress = JSON.parse(fs.readFileSync("my-address.json", 'utf-8'));
var targetIndex = Number(process.argv.splice(2)[0]);

console.log(`Sending ONLYONE to target ${targetIndex}.`);

async function sendOnlyone(fromAddress, toAddress) {

    var Tx = require('ethereumjs-tx').Transaction;
    var Web3 = require('web3');
    var web3 = new Web3(new Web3.providers.HttpProvider('https://bsc-dataseed.binance.org/'));

    var amount = web3.utils.toHex(10);
    var privateKey = Buffer.from(myAddress.privateKey, 'hex');
    var abiArray = JSON.parse(JSON.parse(fs.readFileSync('onlyone-abi.json','utf-8')));
    var contractAddress = '0xb899db682e6d6164d885ff67c1e676141deaaa40'; // ONLYONE address
    var contract = new web3.eth.Contract(abiArray, contractAddress, {from: fromAddress});
    var Common = require('ethereumjs-common').default;
    var BSC_FORK = Common.forCustomChain(
        'mainnet',
        {
        name: 'Binance Smart Chain Mainnet',
        networkId: 56,
        chainId: 56,
        url: 'https://bsc-dataseed.binance.org/'
        },
        'istanbul',
    );

    var count = await web3.eth.getTransactionCount(myAddress);

    var rawTransaction = {
        "from":myAddress,
        "gasPrice":web3.utils.toHex(5000000000),
        "gasLimit":web3.utils.toHex(210000),
        "to":contractAddress,"value":"0x0",
        "data":contract.methods.transfer(toAddress, amount).encodeABI(),
        "nonce":web3.utils.toHex(count)
    };

    var transaction = new Tx(rawTransaction, {'common':BSC_FORK});
    transaction.sign(privateKey)

    var result = await web3.eth.sendSignedTransaction('0x' + transaction.serialize().toString('hex'));
    return result;
}

sendOnlyone(myAddress, targetAccounts[targetIndex]);

Encontré esta documentación en uno de los proyectos que estoy usando en mi dapp: https://developers.fortmatic.com/docs/smart-contract-functions

La documentación explica bastante detalladamente qué hacer paso a paso para enviar transferencias de tokens ERC20 para versiones web3 anteriores a la 1.0 (0.20.x) y posteriores a la 1.0.

Aquí hay una vista previa de cómo se ve la versión 0.20.x del código web3.

// Initialize provider
import Fortmatic from 'fortmatic';
import Web3 from 'web3';

const fm = new Fortmatic('YOUR_API_KEY');
window.web3 = new Web3(fm.getProvider()); // Can replace with MetaMask web3 provider

// Get the contract ABI from compiled smart contract json
const erc20TokenContractAbi = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"tokens","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"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":"tokens","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdrawEther","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"_totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"tokenOwner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"acceptOwnership","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":true,"inputs":[{"name":"a","type":"uint256"},{"name":"b","type":"uint256"}],"name":"safeSub","outputs":[{"name":"c","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"tokens","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"a","type":"uint256"},{"name":"b","type":"uint256"}],"name":"safeDiv","outputs":[{"name":"c","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"tokens","type":"uint256"},{"name":"data","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"a","type":"uint256"},{"name":"b","type":"uint256"}],"name":"safeMul","outputs":[{"name":"c","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"newOwner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"tokenAddress","type":"address"},{"name":"tokens","type":"uint256"}],"name":"transferAnyERC20Token","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"tokenOwner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"a","type":"uint256"},{"name":"b","type":"uint256"}],"name":"safeAdd","outputs":[{"name":"c","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"tokens","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"tokenOwner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"tokens","type":"uint256"}],"name":"Approval","type":"event"}];

// Create contract object
const tokenContract = web3.eth.contract(erc20TokenContractAbi);
// Instantiate contract
const tokenContractInstance = tokenContract.at('0x8EBC7785b83506AaA295Bd9174e6A7Ad5681fb80');

const toAddress = '0xE0cef4417a772512E6C95cEf366403839b0D6D6D';
// Calculate contract compatible value for transfer with proper decimal points using BigNumber
const tokenDecimals = web3.toBigNumber(18);
const tokenAmountToTransfer = web3.toBigNumber(100);
const calculatedTransferValue = web3.toHex(tokenAmountToTransfer.mul(web3.toBigNumber(10).pow(tokenDecimals)));

// Call contract function (non-state altering) to get total token supply
tokenContractInstance.totalSupply.call(function(error, result) {
  if (error) throw error;
  console.log(result);
});

// Get user account wallet address first
web3.eth.getAccounts(function(error, accounts) {
  if (error) throw error;
  // Send ERC20 transaction with web3
  tokenContractInstance.transfer.sendTransaction(toAddress, calculatedTransferValue, {from: accounts[0]}, function(error, txnHash) {
    if (error) throw error;
    console.log(txnHash);
  });
});

Así es como se hace con ethers.js. No sé por qué la mayoría de las otras respuestas usan una importación de un ABI json no especificado cuando ERC20 sigue la misma interfaz para cualquier token, y es poco probable que tenga el ABI de un token público.

Tenga en cuenta que en este ejemplo asumo que la cantidad debe multiplicarse por una potencia de 6 ( utils.parseUnits(n, 6)) que se aplica a USDT y USDC, por ejemplo. Otros tokens ERC20 pueden variar.

import { Contract, providers, utils } from 'ethers'

const erc20abi = [
  /**
   * @dev Returns the amount of tokens in existence.
   */
  'function totalSupply() external view returns (uint256)',

  /**
   * @dev Returns the amount of tokens owned by `account`.
   */
  'function balanceOf(address account) external view returns (uint256)',

  /**
   * @dev Moves `amount` tokens from the caller's account to `recipient`.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * Emits a {Transfer} event.
   */
  'function transfer(address recipient, uint256 amount) external returns (bool)',

  /**
   * @dev Returns the remaining number of tokens that `spender` will be
   * allowed to spend on behalf of `owner` through {transferFrom}. This is
   * zero by default.
   *
   * This value changes when {approve} or {transferFrom} are called.
   */
  'function allowance(address owner, address spender) external view returns (uint256)',

  /**
   * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * IMPORTANT: Beware that changing an allowance with this method brings the risk
   * that someone may use both the old and the new allowance by unfortunate
   * transaction ordering. One possible solution to mitigate this race
   * condition is to first reduce the spender's allowance to 0 and set the
   * desired value afterwards:
   * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
   *
   * Emits an {Approval} event.
   */
  'function approve(address spender, uint256 amount) external returns (bool)',

  /**
   * @dev Moves `amount` tokens from `sender` to `recipient` using the
   * allowance mechanism. `amount` is then deducted from the caller's
   * allowance.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * Emits a {Transfer} event.
   */
  'function transferFrom(address sender, address recipient, uint256 amount) external returns (bool)',

  /**
   * @dev Emitted when `value` tokens are moved from one account (`from`) to
   * another (`to`).
   *
   * Note that `value` may be zero.
   */
  'event Transfer(address indexed from, address indexed to, uint256 value)',

  /**
   * @dev Emitted when the allowance of a `spender` for an `owner` is set by
   * a call to {approve}. `value` is the new allowance.
   */
  'event Approval(address indexed owner, address indexed spender, uint256 value)',
]

export async function approveERC20(
  provider: providers.Web3Provider,
  tokenAddr: string,
  spenderAddr: string,
  amount: string
) {
  const signer = provider.getSigner()
  const contract = new Contract(tokenAddr, erc20abi, signer)
  contract.approve(spenderAddr, utils.parseUnits(amount, 6))
}

export async function transferERC20(
  provider: providers.Web3Provider,
  tokenAddr: string,
  recipientAddr: string,
  amount: string
) {
  const signer = provider.getSigner()
  const contract = new Contract(tokenAddr, erc20abi, signer)
  contract.transfer(recipientAddr, utils.parseUnits(amount, 6))
}