Як відправити USDT за допомогою коду на Go

Якщо у вас є криптогаманець, то ви точно хоч раз да відправляли USDT. Але якщо ви починаючий програміст, то рано чи пізно ви захочете зробити в своєму проєкті автоматичну виплату коштів вашим користувачам або взагалі створити свій криптовалютний токен і відправляти його.

В цій статті я розкажу як це зробити, але перед цим вам потрібно знати базу. База полягає в тому, що в EVM блокчейнах існують тільки перекази вбудованої валюти, наприклад в Ethereum це ETH. А ще в блокчейнах є смарт контракти.

Смарт контракт - це маленька програма написана на мові Solidity (в EVM блокчейнах) і виконується така програма на машинах майнерів. Виконання програм в блокчейні платне, плата знімається за кожну виконану ассемблерну функцію.

Так от в якийсь момент розробники придумали стандарт (інтерфейс) який називається ERC-20.

contract ERC20Interface {
    function totalSupply() public constant returns (uint);
    function balanceOf(address tokenOwner) public constant returns (uint balance);
    function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
    function transfer(address to, uint tokens) public returns (bool success);
    function approve(address spender, uint tokens) public returns (bool success);
    function transferFrom(address from, address to, uint tokens) public returns (bool success);

    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

Цей інтерфейс призначений для створення своїх криптовалютних токенів і щоб кожен створював їх за одним стандартом. В цьому стандарті ми бачимо які функції є у всіх криптовалютних токенів, в тому числі в USDT. Дуже часто коли реалізують цей інтерфейс, то в код смарт контракту додають ще функцію mint - для зарахування токенів комусь на баланс та burn - для спалювання. Знайти готову реалізацію ERC-20 стандарту ви можете за посиланням.

В коді реалізації ви можете помітити дуже примітивний код, наприклад це:

mapping(address account => uint256) private _balances;

mapping(address account => mapping(address spender => uint256)) private _allowances;

Ми бачимо що наші “цифрові долари” це звичайна мапа (або як кажуть пайтон розробники - словник), в якій ключ це адреса користувача, а значення це баланс.
Також в контракті є інша мапа allowances - в ній зберігається інформація: кому дозволено списувати токени і скільки для обраної адреси. Це потрібно тоді, коли відправник транзакції з викликом методу transferFrom не є власником рахунку, з якого хочуть списати кошти.

Далі шукаємо код відправки токенів:

function transfer(address to, uint256 value) public virtual returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, value);
        return true;
}

Ми бачимо, що ця функція дістає адресу того, хто відправив транзакцію і далі викликає метод _transfer, якщо вам цікаво, то можете подивитись код, цього методу, але я вам напишу коротко: перевіряє чи є дозвіл на списування вказаної суми, чи є така сума на адресі користувача, якщо все ок то змінює значення балансів і дозволів на списання.

І настав момент питання цієї статті: ЯК ВИКЛИКАТИ ЦЕЙ МЕТОД?

Давайте по крокам.

Для початку нам потрібен гаманець, на якому є валюта блокчейна (бо саме в ній платиться комісія за виклик методів смарт контракту) і самі USDT (або на будь який інший) токени. Для зручності ви можете використовувати ваш гаманець з TrustWallet. Коли ви створювали гаманець, він вам запропонував зберегти 12 слів. Але ці 12 слів ви маєте в коді перетворити на приватний ключ гаманця, яким будете підписувати транзації перед відправкою в блокчейн. Для цього нам потрібні наступні бібліотеки:

go get github.com/foxnut/go-hdwallet
go get github.com/ethereum/go-ethereum

Почнемо з того, що отримаємо приватний ключ нашого гаманця:

master, err := hdwallet.NewKey(
	hdwallet.Mnemonic("Ваші 12 слів"),
)
if err != nil {
	log.Fatal("initialize master wallet private key: ", err)
}
senderAddress := crypto.PubkeyToAddress(*master.PublicECDSA)
fmt.Println(senderAddress)
evmWallet, err := master.GetWallet(hdwallet.CoinType(hdwallet.ETH))
if err != nil {
	log.Fatal("initialize ETH wallet: ", err)
}

Наступним кроком нам потрібно зібрати транзакцію на виклик методу смарт контракту. На рівні блокчейну це просто байти які ми передаємо в правильному порядку. Виклик методу це: 4 байти хеш Keccak256Hash від сігнатури методу + кожен аргумент це 32 байти. Звучить складно? Тоді давайте перейдемо до написання коду, нам потрібно порахувати хеш методу transfer(address to, uint tokens). Сігнатура методу складається з назви і типів даних аргументів в круглих дужках через кому. В нашому випадку сігнатура:

transfer(address,uint256)

Тепер від сігнатури треба порахувати хеш Keccak256Hash і взяти перші 4 байти цього хешу:

sig := []byte("transfer(address,uint256)")
hash := crypto.Keccak256Hash(sig)
fmt.Println(hash.String())
methodID := hash[:4]
fmt.Println(methodID)

Цей код вам виведе:

0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
[169 5 156 187]

ось ці 4 байти збережіть собі як змінну на рівні пакету. Наприклад:

package service

var TransferMethod = []byte{169, 5, 156, 187}

як я писав вище, кожен аргумент незалежно від його типу даних займає 32 байти. Тому тіло нашої транзакції буде дорівнювати: 4+32+32. Давайте створимо слайс байтів в який ми запишемо дані транзакції:

data := make([]byte, 4+32+32)

Далі давайте запишимо в наш слайс метод, який ми викликаємо:

copy(data, TransferMethod)

Далі нам потрібно адресу отримувача токенів перетворити на 32 байти, для цього виконайте наступний код:

address := common.HexToAddress("адрес отримувача")
addressBytes := common.LeftPadBytes(address.Bytes(), 32)

Вам цікаво, навіщо функція common.LeftPadBytes. Справа в тому, що EVM адреса займає 20 байтів, але якщо ми їх просто додамо в наш слайс байтів, то отримаємо помилку, бо в EVM блокчейнах старші біти знаходяться справа, тому в 32 байтовому слайсі нам треба наші 20 байт посунути в кінець, що і робить функція common.LeftPadBytes.

Далі адресу отримувача в байтовому вигляді потрібно додати в наш масив даних транзакції, для цьго виконуємо наступний код:

copy(data[4:], common.LeftPadBytes(address.Bytes(), 32))

Наступним кроком нам потрібно нашу суму переказу, наприклад 10.2 USDT перевести найменшу одиницю виміру (наприклад у гривні це копійка). Але не спішіть множити число на 10 в 2 степені, бо у USDT кількість символів після крапки це число, яке зберігається в смарт контракті і в кожній мережі може бути різне. Для того, щоб дізнатись скільки цифр після крапки у USDT в мережі Ethereum, перейдіть за посиланням.

Розкривши слайдер, ви побачите, що в мережі Ethereum у USDT 6 знаків після крапки. Для роботи з такими великими числами я використовую бібліотеку: github.com/shopspring/decimal

amountWithoutDecimals := amount.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(6))))

В цьому коді я перевожу число в мінімальну одиницю, з якою оперує смарт контракт. Давайте тепер суму переказу додамо до даних нашої майбутньої транзакції:

copy(data[4+32:], common.LeftPadBytes(amountWithoutDecimals.BigInt().Bytes(), 32))

Дані нашої транзакції готові, у вас мало вийти щось типу такого (далі вставляю свій код, він трошки відрізняється від рядків коду показаних вище):

data := make([]byte, 4+32+32)
copy(data, TransferMethod)
copy(data[4:], common.LeftPadBytes(address.Bytes(), 32))
copy(data[4+32:], common.LeftPadBytes(amountWithoutDecimals.BigInt().Bytes(), 32))

Наступним кроком вам потрібно отримати nonce. Це такий собі лічильник транзакцій відправлених з гаманця, він потрібен щоб одна й та сама транзакція не була відправлена повторно.

block, err := client.BlockByNumber(ctx, nil)
if err != nil {
	return nil, fmt.Errorf("get last block number: %w", err)
}
nonce, err := client.NonceAt(context.Background(), myAddress, block.Number())
if err != nil {
	return nil, fmt.Errorf("get nonce: %w", err)
}

Це приклад мого коду, у вас може не бути return nil.

Далі вам потрібно отримати з блокчейну параметри комісій, щоб вашу транзакцію точно включили в блок, ви можете просто використовувати мою функцію, якщо буде цікаво, то розберетесь як вона працює.

func (s *Service) getTransactionGasParams(cl *ethclient.Client, ctx context.Context, block *types.Block) (*big.Int, *big.Int, error) {
	baseFee := block.BaseFee()
	if baseFee == nil {
		return nil, nil, fmt.Errorf("get block base fee")
	}

	maxPriorityFee, err := cl.SuggestGasTipCap(ctx)
	if err != nil {
		return nil, nil, fmt.Errorf("get max priority fee per gas: %w", err)
	}

	maxFeePerGas := big.NewInt(0).Add(baseFee, maxPriorityFee)
	plus := new(big.Int).SetUint64(baseFee.Uint64() * 250 / 100)
	maxFeePerGas.Add(maxFeePerGas, plus)
	log.Debugf("-------- debug gas info --------")
	log.Debugf("block.numer:    %d", block.Number().Uint64())
	log.Debugf("baseFee:        %d", baseFee.Uint64())
	log.Debugf("maxPriorityFee: %d", maxPriorityFee.Uint64())
	log.Debugf("maxFeePerGas:   %d", maxFeePerGas.Uint64())
	log.Debugf("--------------------------------")
	return maxPriorityFee, maxFeePerGas, nil
}
maxPriorityFee, maxFeePerGas, err := s.getTransactionGasParams(cl, ctx, block)
	if err != nil {
		return nil, fmt.Errorf("s.getTransactionGasParams: %w", err)
	}
	gasLimit, err := client.EstimateGas(ctx, ethereum.CallMsg{
		From:       myAddress,
		To:         &token.Address,
		Gas:        0,
		GasFeeCap:  maxFeePerGas,
		GasTipCap:  maxPriorityFee,
		Data:       data,
		AccessList: nil,
	}) 

В коді показаному вище, ми отримуємо параметрі комісій з блокчейну і передаємо дані в client.EstimateGas - це дуже корисна функція, вона імітує запит локально у вас на комп’ютері і перевіряє чи ви все зробили правильно, також ця функція перевірить чи пройде транзакція з вказаними даними, бо функція яку ви викликаєте може повернути помилку і платіж не пройде, наприклад, якщо ви ввели суму переказу якої у вас немає. В token.Address у вас має бути адреса USDT контракту.

Якщо помилок не було, створюємо транзакцію і підписуємо її після чого відправляємо в блокчейн.

chainID, err := cl.ChainID(ctx)
	if err != nil {
		return nil, fmt.Errorf("get chain id: %w", err)
	}
blockchainTx := types.NewTx(&types.DynamicFeeTx{
	ChainID:   chainID,
	Nonce:     nonce,
	GasFeeCap: maxFeePerGas,
	GasTipCap: maxPriorityFee,
	Gas:       gasLimit,
	To:        &token.Address,
	Value:     big.NewInt(0),
	Data:      data,
})

signedTx, err := types.SignTx(blockchainTx, types.LatestSignerForChainID(chainID), p)
if err != nil {
	return nil, fmt.Errorf("sign tx: %w", err)
}

if err := client.SendTransaction(ctx, signedTx); err != nil {
	return nil, fmt.Errorf("send transaction: %w", err)
}

Вітаю, якщо ви відправили кодом свої перші USDT. Щоб знайти вашу транзакцію, просто введіть її хеш

signedTx.Hash().Hex()

на сайті etherscan або на іншому оглядачу блокчейну.

Якщо у вас залишись питання ви можете звернутись до мене в приватні повідомлення телеграм @kbgod, де я вас безкоштовно проконсультую, якщо ви робите соціально важливий проєкт.

Дякую за увагу, чекаю на коментарі)

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Askold
Askold@askoldex

219Прочитань
0Автори
7Читачі
На Друкарні з 20 квітня

Більше від автора

Вам також сподобається

  • Ethereum та смарт-контракти

    Ethereum це децентралізована платформа, що дозволяє створювати та виконувати смарт-контракти. Вона була розроблена як розширення можливостей блокчейну Bitcoin і швидко набула популярності завдяки своїй універсальності та багатофункціональності.

    Теми цього довгочиту:

    Криптовалюта
  • Why blockchain technology in healthcare is key to fraud prevention

    Blockchain technology enhances healthcare security by preventing fraud and ensuring data integrity. It decentralizes records, reducing risks of tampering and unauthorized access. From billing to drug supply chains, blockchain creates a transparent and trustworthy system.

    Теми цього довгочиту:

    Blockchain

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається

  • Ethereum та смарт-контракти

    Ethereum це децентралізована платформа, що дозволяє створювати та виконувати смарт-контракти. Вона була розроблена як розширення можливостей блокчейну Bitcoin і швидко набула популярності завдяки своїй універсальності та багатофункціональності.

    Теми цього довгочиту:

    Криптовалюта
  • Why blockchain technology in healthcare is key to fraud prevention

    Blockchain technology enhances healthcare security by preventing fraud and ensuring data integrity. It decentralizes records, reducing risks of tampering and unauthorized access. From billing to drug supply chains, blockchain creates a transparent and trustworthy system.

    Теми цього довгочиту:

    Blockchain