Можно получить произвольное количество приватных ключей и адресов из пароля. Разумеется, пароль должен быть надёжным, чтобы тебя не взломали.
Код на NodeJS. Используем библиотеку ethers.
npm i ethers
import { createHash } from 'crypto'
import { ethers } from 'ethers'
import { password } from './password.js'
const NUMBER_OF_WALLETS = 10
// Приватный ключ Ethereum — это строка из 64 шестнадцатеричных цифр
// с приставкой "0x" (то есть всего 66 символов).
// Лучший способ получить 64 шестнадцатеричные цифры из энтропии —
// — это вычислить хеш SHA-256.
const textToPrivateKey = text => '0x' + createHash('sha256')
.update(Buffer.from(text))
.digest('hex')
// Библиотека ethers умеет создать офлайн-кошелёк (wallet) из приватного ключа.
const privateKeyToWallet = privateKey => new ethers.Wallet(privateKey)
// Превращаем свой пароль в список строк по шаблону "пароль/индекс_строки".
// Каждую эту строку превращаем в приватный ключ.
// Каждый приватный ключ превращаем в офлайн-кошелёк.
const wallets = Array
.from({ length: NUMBER_OF_WALLETS }, (_, i) => `${password}/${i}`)
.map(textToPrivateKey)
.map(privateKeyToWallet)
// Можно вывести адреса и приватные ключи кошельков для проверки работы кода.
wallets.forEach(wallet => console.log(wallet.address, wallet.privateKey))
Для сравнения результата, вот первые три Ethereum-адреса для пароля "123":
0x8f91FF49752cf29Ba28C4EA8E115eCa12EC828E1
0xA9B8BC517dcDBd50B2E78979353d120AC77369Eb
0xa2499c890A11b211008a9D03566d65003e8B9858
Существует возможность привести адрес Ethereum к формату международного номера банковского счёта. Нужно нам такое или нет — большой вопрос, так как эту функциональность не очень-то широко используют в мире Ethereum.
Не каждый адрес может быть приведён к валидному IBAN, а только такой, который начинается с нулевого байта (0x00...). Подобрать такой адрес несложно, вот примеры строк, хеш которых, в качестве приватного ключа, даёт подходящий адрес:
4o -> 0x002f8467e0A23F43895E5927609c005508b9376D
fn -> 0x002224Fe5A34c6Ef8E1AECE287941EDDFa5E5539
ln -> 0x008aC136212231cA8DAf072f2291f509B8293E2D
qc -> 0x00eC9194702107C7fA7FA18E913dEb5f1AcbA3A0
qk -> 0x0090Aa969F52C8F5217F92Fa1Ae8E503c581a905
Итак, подбираем подходящий адрес и далее:
// Преобразование обычного адреса в ICAP
const icapAddress = ethers.getIcapAddress(wallet.address)
// Получение обычного адреса из ICAP
const address = ethers.getAddress(icapAddress)
Ранее мы говорили о IBAN, но тут видим ICAP. ICAP — это формат адреса,
построенный по тем же принципам, что и IBAN, но без ограничения по длине.
ICAP длиною 34 символа — это IBAN. ICAP длиною более 34 символов
— это просто ICAP. Если мы передадим в функцию getIcapAddress
адрес, который
начинается с ненулевого байта, получим ICAP-адрес длиною 35 символов.
Пример IBAN — XE500S3L3E0PENA26T24BGIFYLR6QZG1NX
.
Балансы хранятся в блокчейнах (как правило, в интернете). Для доступа к блокчейну используются провайдеры.
Основная сеть Ethereum называется MainNet. Вот простейший способ получить провайдер основной сети:
const provider = ethers.getDefaultProvider('mainnet')
Чтобы получить провайдер тестовой сети, необходимо указать её имя:
// Goerli — одна из тестовых сетей Ethereum.
const provider = ethers.getDefaultProvider('goerli')
Запрашиваем балансы созданных ранее офлайн-кошельков:
// Поскольку это сетевой запрос, метод получения баланса асинхронный.
// Сам кошелёк тоже включаем в результат, чтобы было понятно, чей это баланс.
const addBalanceToWallet = async wallet => ({
wallet,
balance: await provider.getBalance(wallet.address),
})
// Можно вывести адреса и их балансы в консоль.
Promise
.all(wallets.map(addBalanceToWallet))
.then(result => {
result.forEach(({ wallet, balance }) => {
console.log(wallet.address, balance)
})
})
В общем случае объект транзакции имеет интерфейс:
interface TransactionRequest {
to: string // адрес получателя
value: bigint // количество единиц wei, которое мы отправляем
gasLimit: bigint // количество газа, которое мы тратим на операцию
gasPrice: bigint // цена газа в wei
}
- Это не полный интерфейс
TransactionRequest
в библиотекеethers
, там ещё есть куча полей. Но основные поля — именно эти. - Вместо
bigint
подходят такжеnumber
иstring
(число, написанное строкой). Но для работы с эфиром удобнее всегоbigint
.
Количество газа для отправки транзакции фиксированное — 21000. Цена газа постоянно меняется. Посмотреть актуальную можно на etherscan.
Отправляет транзакцию онлайн-кошелёк. Создаётся почти также, как офлайн, но вторым параметром необходимо передать провайдер:
const wallet = new ethers.Wallet(privateKey, provider)
// Поскольку value ожидает сумму в wei, используем parseEther для перевода.
// Можем указать gasLimit обычным числом (number), так как это просто 21000.
// Цену газа обычно пишут в gwei, используем parseUnits для перевода в wei.
const transaction = {
to: '0x66Ea28ea0fC81D40E31937e0F80b8F4d24e1adDA',
value: ethers.parseEther('0.01'),
gasLimit: 21000,
gasPrice: ethers.parseUnits('50', 'gwei'),
}
// Онлайн-кошелёк знает свой приватный ключ и подпишет им транзакцию.
// Выводим в консоль всякое; если ошибки не было, значит всё прошло нормально.
wallet
.sendTransaction(transaction)
.then(response => {
console.log('transaction hash:', response.hash)
return provider.waitForTransaction(response.hash)
})
.then(receipt => {
console.log('receipt:', receipt)
})
.catch(error => {
console.log('error:', error.message)
})
Отправить совсем всю сумму невозможно, так как надо платить комиссию. Значит, надо рассчитать сумму комиссии и вычесть её из баланса адреса.
// Сначала получим баланс, чтобы знать, какой суммой располагает адрес.
provider.getBalance(wallet.address).then(balance => {
// Объявляем gasLimit типом bigint, потому что он участник вычислений.
const gasLimit = 21000n
const gasPrice = ethers.parseUnits('30', 'gwei')
const fee = gasLimit * gasPrice
const value = balance - fee
const transaction = {
to: '0x66Ea28ea0fC81D40E31937e0F80b8F4d24e1adDA',
value,
gasLimit,
gasPrice,
}
// И отправляем эту транзакцию...
})
Этот пример написан на языке Solidity. Если нет комментария на первой строке с указанием лицензии, IDE будут ругаться, так что добавляем MIT.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyFaucet {
address public owner;
// Конструктор выполняется один раз в жизни, в момент развёртывания.
// Здесь мы записываем в переменную owner адрес создателя смарт-контракта
// (то есть тот адрес, с которого произошла транзакция развёртывания).
constructor() {
owner = msg.sender;
}
// Эту функцию объявляем, чтобы смарт-контракт мог принимать эфир.
receive() external payable {}
// Область видимости функции "external" означает, что обратиться к ней можно
// Только "снаружи", то есть вызвать при помощи транзакции.
// Это потребует меньше газа, чем обратиться к public-функции,
// но external-функцию нельзя вызывать в коде контракта. А мы и не вызываем.
function withdraw(uint amount) external {
// Если require принимает false первым аргументом,
// выполнение контракта прекращается с сообщением из второго аргумента.
require(msg.sender == owner, 'You are not the owner');
require(
amount <= 0.01 ether,
'Cannot withdraw more than 0.01 ETH at a time'
);
// Необходимо привести msg.sender к типу payable перед вызовом transfer.
payable(msg.sender).transfer(amount);
}
}
Если у нас установлен NodeJS, проще всего скомпилировать при помощи npx
:
npx solc@latest --bin --abi FileName.sol
В результате появятся два файла:
FileName_sol_ContractName.abi
FileName_sol_ContractName.bin
import { readFileSync } from 'fs'
import { ethers } from 'ethers'
import { privateKey } from './private-key.js'
const abiFileName = 'FileName_sol_ContractName.abi'
const binFileName = 'FileName_sol_ContractName.bin'
const abi = JSON.parse(readFileSync(abiFileName, 'utf8'))
const bin = readFileSync(binFileName, 'utf8')
const provider = ethers.getDefaultProvider('goerli')
const wallet = new ethers.Wallet(privateKey, provider)
const deployContract = async () => {
const factory = new ethers.ContractFactory(abi, bin, wallet)
const contract = await factory.deploy()
const address = await contract.getAddress()
// Адрес контракта становится известен раньше завершения развёртывания.
console.log('Address will be', address)
console.log('Please wait...')
await contract.waitForDeployment()
console.log('Successfully deployed!')
}
deployContract().catch(error => console.log('error:', error))
// Тот же самый abi, что и в предыдущем примере кода
const contract = new ethers.Contract(contractAddress, abi, provider)
// Подключаем контракт к онлайн-кошельку
const contractWithWallet = contract.connect(wallet)
const readOwner = async () => {
// Прочитать публичное поле можно без транзакции, так что используем contract.
const owner = await contract['owner']()
console.log('owner:', owner)
}
const withdraw = async () => {
const amount = 1_000_000_000_000_000n // 0.001 ETH
// Чтобы вызвать метод, используем contractWithWallet, ибо нужна транзакция.
// Числовой параметр передаём обязательно в виде строки.
const response = await contractWithWallet['withdraw'](amount.toString())
console.log('transaction hash:', response.hash)
const receipt = await provider.waitForTransaction(response.hash)
console.log('receipt:', receipt)
}
readOwner().then(withdraw).catch(error => console.log('error:', error.message))
ERC20 является перечнем требований к смарт-контрактам, выпускающим токены. Если смарт-контракт выполняет эти требования, он является ERC20. В качестве примера рассматриваем USDT.
Адрес контракта USDT: 0xdAC17F958D2ee523a2206206994597C13D831ec7
.
ABI можно найти на вкладке Contract
.
В качестве адреса для хранения таких токенов используем наш адрес Ethereum. Отправить транзакцию токенов ERC20 на другой адрес можно следующим образом:
// Создаём контракт, передав 3-м аргументом кошелёк, так как будет транзакция.
const contract = new ethers.Contract(contractAddress, abi, wallet)
// У каждого ERC20-токена "количество знаков после запятой" может быть разным.
// У USDT 6 знаков после запятой.
// Чтобы отправить 100 баксов, необходимо выполнить следующее преобразование.
const amount = ethers.parseUnits('100', 6)
// Метод transfer является стандартным методом ERC20.
// Принимает адрес получателя и количество.
contract['transfer'](recipientAddress, amount)
.then(response => {
console.log('transaction hash:', response.hash)
return provider.waitForTransaction(response.hash)
})
.then(receipt => {
console.log('receipt:', receipt)
})
.catch(error => console.log('error:', error.message))