Для начала определимся, что мы хотим получить.

  1. Авторизация с кошельком через metamask extension.

  2. Магазин внутри-игровых предметов. При покупке, осуществляется перевод средств на адрес магазина и записывается адрес покупателя и id предмета в контракт. Можно запросить купленные предметы.

Запрос транзакции
Запрос транзакции
Завершение транзакции
Завершение транзакции

Фреймворк для разработки и тестирования

Во-первых, понадобятся инструменты для сборки, тестирования, отладки и публикации смарт-контрактов. Современные решения: foundry и hardhat3. Я использовал foundry.

Для разработки также потребуется тестнет. Можно использовать стороннее решение, или воспользоваться тестнетом представляемым фреймворком. Я использовал anvil из foundry.

anvil запускается и работает из консоли. Функционал стандартный, тестнет создает аккаунты со средствами на счетах и предоставляет RPC точку(вида, http://127.0.0.1:8545) для работы. По этому адресу нужно подключить metamask extension. А также туда публикуется собранный контракт.

Тестнет anvil
Тестнет anvil
Подключение тестнета в metamask extension
Подключение тестнета в metamask extension

Сборка и публикация

Контракт будет написан на solidity. Нам понадобятся две функции:

1. Покупка предмета. Покупка будет передавать средства на кошелек магазина и записывать покупателю id купленного предмета.

function purchaseItem(uint8 _itemId) external payable {    
  uint256 itemCost = 2 gwei; // 0.0012 USD / 0.18 RUB;    
  // Forward the funds to the seller
  (bool success, ) = STORE_ADDRESS.call{value: itemCost}("");    
  require(success, "Transfer failed");    
  boughtItems[msg.sender].push(_itemId);
}

2. Просмотр купленных предметов.

function getBoughtItems() external view returns (uint8[] memory) {    
  return boughtItems[msg.sender];
}

Также запишем адрес кошелька магазина при инициализации контракта.

constructor(address _store) {    
  STORE_ADDRESS = _store;
}

Для сборки, тестирования и публикации я использовал forge из фреймворка foundry. Тесты для дураков приводить здесь не буду. После успешного тестирования и сборки, публикуем контракт:

forge create ./src/Store.sol:Store \    
--private-key <PRIVATE_KEY> \    
--rpc-url <RPC_URL> \    
--broadcast \    
--constructor-args <STORE_PUBLIC_KEY>

После публикации, нам предоставляют адрес смарт-контракта. Строка "Deployed to". Его будем использовать в следующей части при подключении интерфейса.

Результат успешной публикации смарт-контракта
Результат успешной публикации смарт-контракта

Интерфейс магазина

Понадобится библиотека для взаимодействия с metamask и блокчейном. Я использовал ethers v6, она показалась мне наиболее хорошо документированной.

Подключаемся к кошельку через metamask extension.

import { ethers } from "ethers";
const provider = new ethers.BrowserProvider(window.ethereum); 

Запросим баланс, чтобы убедится в корректной работе.

const signerObj = await provider.getSigner();
const balance = await provider.getBalance(signerObj.address);        
const balanceFormatted = ethers.formatEther(balance);

Взаимодействие со смарт-контрактом

1. Нам нужен abi собранного контракта. Это строка с описанием методов смарт-контракта, его можно написать самостоятельно или взять в артефакте после сборки.

import ContractArtifact from "***/Store.json";
const abi = ContractArtifact.abi;

2. Создаем экземпляр Contract, передаем туда адрес смарт-контракта, который выдали после сборки, abi и объект полученный при авторизации.

const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, signerObj);

3. Теперь можно вызывать функции смарт-контракта.

// покупка предмета
contract.purchaseItem(itemId, {    
  value: ethers.parseUnits("2", "gwei")
});
// получение списка покупок
const userItems = await contract.getBoughtItems();

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

Ссылки
И злополучная книга

Всем спасибо за внимание!

Комментарии (6)


  1. HemulGM
    31.01.2026 08:26

    А где хранятся "товары", который показываются пользователю?


    1. ALapinskas Автор
      31.01.2026 08:26

      Данные хранятся непосредственно в блокчейне(on-chain) в переменной boughtItems.
      Она инициализируется в начале листинга смарт-контракта и представляет из себя маппинг кошельков пользователей и купленных ими предметов.

      mapping(address => uint8[]) private boughtItems;


      1. HemulGM
        31.01.2026 08:26

        Откуда изначально карточки товаров берутся? Откуда картинки, описание? При чем тут купленные товары


        1. ALapinskas Автор
          31.01.2026 08:26

          Все зашито в интерфейс, написан он на html-css-react. Кусок кода:

          const STORE_ITEMS = {
          	KNIGHT_TRAINING: {
          		ID: 1,
          		TITLE: "Knight training",
          		DESCRIPTION: "Additional melee hit for knight. +3 damage on every 3rd hit."
          	},
          	KNIGHT_SWORD_SHARPENING: {
          		ID: 2,
          		TITLE: "Knight sword sharpening",
          		DESCRIPTION: "Additional melee damage for knight. +1 damage on every hit."
          	},
          	ARCHER_FLAMING_ARROWS: {
          		ID: 3,
          		TITLE: "Archer flaming arrows",
          		DESCRIPTION: "Archer burning arrows. +5 damage for buildings."
          	} 
          }
          
          <div className="store-card">
          	{ isLoading ? 
          	<Spinner size="lg" /> :
          	<Stack gap="4" direction="row" wrap="wrap">
          	<Card.Root width="320px" key={1}>
          		<Card.Body gap="2">
          		<Avatar.Root size="lg" shape="rounded">
          			<Avatar.Image src="./assets/tool_sword_b.png" />
          			<Avatar.Fallback name={STORE_ITEMS.KNIGHT_TRAINING.TITLE} />
          		</Avatar.Root>
          		<Card.Title mb="2">{STORE_ITEMS.KNIGHT_TRAINING.TITLE}</Card.Title>
          		<Card.Description>{STORE_ITEMS.KNIGHT_TRAINING.DESCRIPTION}</Card.Description>
          		</Card.Body>
          		<Card.Footer justifyContent="flex-end">
          			{ items.has(STORE_ITEMS.KNIGHT_TRAINING.ID) ? 
          			  "item bought"
          			  :
          				<Button onClick={() => buyAction(STORE_ITEMS.KNIGHT_TRAINING.ID)}>Buy (2 gwei)</Button>
          			}
          		</Card.Footer>
          	</Card.Root>
          	...
          </div>

          Купленные товары - это как раз важно, про это и статья.

          Как провести блокчейн-транзакцию и как потом узнать, что человек купил.


          1. HemulGM
            31.01.2026 08:26

            В данном случае, ваша "база данных" - это html файлы. Глупо и не практично. Такой магазин "без базы данных" можно сделать и без блокчейна. Просто на статичных страничках.


  1. tema_rebel
    31.01.2026 08:26

    `mapping(address => uint8[]) private boughtItems;`
    интересный факт: в таком маппинге хранится, сюрприз, массив uint256 - конкретно в данном случае нет упаковки, которую (возможно) автор хотел