китаев.tech

ERC-1155: balanceOf и safeTransferFrom

Как ERC-1155 хранит балансы по tokenId: прочитать balanceOf, отправить часть токенов через safeTransferFrom и дождаться receipt.

ERC-1155 хранит баланс для каждого id

ERC-1155 — стандарт для коллекций, где один контракт может хранить много разных токенов.

balanceOf(account, id) читает баланс адреса по конкретному id. safeTransferFrom(from, to, id, amount, data) переводит количество токенов этого id от одного адреса к другому.


Переводим ERC-1155 токен

Создайте проект:

bash
mkdir erc1155-transfer-example
cd erc1155-transfer-example

npm init -y
npm pkg set type="module"
npm install ethers solc tsx
npm install --save-dev hardhat typescript @types/node
mkdir contracts scripts

Создайте файл hardhat.config.ts:

typescript
import { defineConfig } from "hardhat/config";

export default defineConfig({
  solidity: {
    version: "0.8.20",
  },
});

Создайте файл contracts/Demo1155.sol:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC1155Receiver {
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    ) external returns (bytes4);
}

contract Demo1155 {
    mapping(uint256 => mapping(address => uint256)) private balances;
    mapping(address => mapping(address => bool)) private operatorApprovals;

    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 value
    );

    event ApprovalForAll(address indexed account, address indexed operator, bool approved);

    function mint(address to, uint256 id, uint256 amount) external {
        require(to != address(0), "mint to zero address");
        require(amount > 0, "zero amount");

        balances[id][to] += amount;

        emit TransferSingle(msg.sender, address(0), to, id, amount);
    }

    function balanceOf(address account, uint256 id) external view returns (uint256) {
        require(account != address(0), "balance query for zero address");

        return balances[id][account];
    }

    function setApprovalForAll(address operator, bool approved) external {
        require(operator != msg.sender, "approve to caller");

        operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function isApprovedForAll(address account, address operator) public view returns (bool) {
        return operatorApprovals[account][operator];
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external {
        require(to != address(0), "transfer to zero address");
        require(from == msg.sender || isApprovedForAll(from, msg.sender), "not approved");
        require(balances[id][from] >= amount, "insufficient balance");

        balances[id][from] -= amount;
        balances[id][to] += amount;

        emit TransferSingle(msg.sender, from, to, id, amount);

        if (to.code.length > 0) {
            bytes4 response = IERC1155Receiver(to).onERC1155Received(
                msg.sender,
                from,
                id,
                amount,
                data
            );

            require(
                response == IERC1155Receiver.onERC1155Received.selector,
                "receiver rejected tokens"
            );
        }
    }
}

Создайте файл scripts/transfer-1155.ts:

typescript
import { readFileSync } from "node:fs";
import { ethers } from "ethers";
import solc from "solc";

const source = readFileSync("contracts/Demo1155.sol", "utf8");

const input = {
  language: "Solidity",
  sources: {
    "Demo1155.sol": {
      content: source,
    },
  },
  settings: {
    outputSelection: {
      "*": {
        "*": ["abi", "evm.bytecode.object"],
      },
    },
  },
};

const output = JSON.parse(solc.compile(JSON.stringify(input)));
const contract = output.contracts["Demo1155.sol"].Demo1155;

const abi = contract.abi;
const bytecode = `0x${contract.evm.bytecode.object}`;

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");

// Первый тестовый аккаунт Hardhat. Он получит стартовый баланс.
const sender = new ethers.Wallet(
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
  provider,
);

// Второй тестовый аккаунт Hardhat. Он получит часть ERC-1155 токенов.
const recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";

const factory = new ethers.ContractFactory(abi, bytecode, sender);
const token = await factory.deploy();

await token.waitForDeployment();

const tokenAddress = await token.getAddress();
const tokenId = 7n;
const initialAmount = 100n;
const transferAmount = 25n;

const mintTx = await token.mint(sender.address, tokenId, initialAmount);
await mintTx.wait();

const senderBefore = await token.balanceOf(sender.address, tokenId);
const recipientBefore = await token.balanceOf(recipient, tokenId);

console.log("ERC-1155 address:", tokenAddress);
console.log("Token ID:", tokenId.toString());
console.log("Sender before:", senderBefore.toString());
console.log("Recipient before:", recipientBefore.toString());

const tx = await token.safeTransferFrom(
  sender.address,
  recipient,
  tokenId,
  transferAmount,
  "0x",
);

console.log("Transfer hash:", tx.hash);

const receipt = await tx.wait();

if (!receipt || receipt.status !== 1) {
  throw new Error("Транзакция safeTransferFrom() не была успешно выполнена");
}

const senderAfter = await token.balanceOf(sender.address, tokenId);
const recipientAfter = await token.balanceOf(recipient, tokenId);

console.log("Block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Sender after:", senderAfter.toString());
console.log("Recipient after:", recipientAfter.toString());

Запустите локальную сеть Hardhat в отдельном терминале:

bash
npx hardhat node

Во втором терминале выполните скрипт:

bash
npx tsx scripts/transfer-1155.ts

Скрипт создаст ERC-1155 контракт, выпустит 100 токенов с id = 7, переведёт 25 получателю и проверит балансы по тому же id.


Как это работает

balances[id][account] — хранит баланс конкретного адреса для конкретного token id.

mint(sender.address, tokenId, initialAmount) — создаёт количество токенов с id = 7 на адресе отправителя.

balanceOf(sender.address, tokenId) — читает баланс не просто адреса, а пары address + id.

safeTransferFrom(sender.address, recipient, tokenId, transferAmount, "0x") — переводит 25 единиц token id 7.

"0x" — пустой bytes data. Его можно использовать для дополнительных данных, но в простом переводе он не нужен.

tx.wait() — ждёт, пока перевод попадёт в блок и вернёт receipt.


Почему нужен id

В ERC-20 один контракт обычно представляет один токен:

text
balanceOf(user) = 100 USDC

В ERC-721 каждый tokenId уникален и принадлежит одному владельцу:

text
ownerOf(1) = 0x...

В ERC-1155 один контракт хранит много балансов по разным id:

text
balanceOf(user, 7)  = 100
balanceOf(user, 42) = 3
balanceOf(user, 99) = 0

Поэтому без id нельзя понять, баланс какого именно токена нужно читать или переводить.


Что значит safe

safeTransferFrom проверяет получателя.

Если to — обычный EOA-адрес, перевод проходит сразу. Если to — смарт-контракт, он должен реализовать onERC1155Received и вернуть правильный selector. Это защищает от отправки ERC-1155 токенов в контракт, который не умеет их принимать.


Частые ошибки

Читать balanceOf(account) как в ERC-20 → в ERC-1155 нужен второй аргумент: balanceOf(account, id).

Передавать неверный id → баланс id = 7 не связан с балансом id = 8, даже если они в одном контракте.

Забывать data в safeTransferFrom → пятый аргумент обязателен. Для обычного перевода передавайте "0x".


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.