Як відправити 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

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

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

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

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

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

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