Якщо у вас є криптогаманець, то ви точно хоч раз да відправляли 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, де я вас безкоштовно проконсультую, якщо ви робите соціально важливий проєкт.
Дякую за увагу, чекаю на коментарі)