You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Solidity和Web3.js锁定用户MetaMask钱包中的游戏代币?

区块链井字棋押金锁定与结算实现方案

针对你开发区块链井字棋时的押金锁定需求,以下是基于Solidity和Web3.js的具体实现方案,涵盖合约核心逻辑、前端交互流程及关键注意事项:

一、Solidity合约核心实现

押金锁定的核心思路是将用户的代币(ETH或ERC20)暂存在合约中,待游戏结束后由合约自动完成分配或退还。

1. 原生ETH版本合约

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

contract TicTacToe {
    enum GameStatus { WaitingForOpponent, InProgress, Completed }

    struct Game {
        address player1;
        address player2;
        uint256 stakeAmount;
        GameStatus status;
        address winner;
        uint8[9] board; // 0=空,1=player1,2=player2
    }

    mapping(uint256 => Game) public games;
    uint256 public nextGameId;

    event GameCreated(uint256 gameId, address creator, uint256 stake);
    event GameJoined(uint256 gameId, address player2);
    event GameEnded(uint256 gameId, address winner);

    // 创建游戏:用户存入ETH作为押金,直接锁定在合约
    function createGame() external payable {
        require(msg.value > 0, "押金金额不能为0");
        
        uint256 gameId = nextGameId++;
        games[gameId] = Game({
            player1: msg.sender,
            player2: address(0),
            stakeAmount: msg.value,
            status: GameStatus.WaitingForOpponent,
            winner: address(0),
            board: [0,0,0,0,0,0,0,0,0]
        });

        emit GameCreated(gameId, msg.sender, msg.value);
    }

    // 加入游戏:对手需存入等额ETH
    function joinGame(uint256 gameId) external payable {
        Game storage game = games[gameId];
        require(game.status == GameStatus.WaitingForOpponent, "游戏不处于等待对手状态");
        require(msg.sender != game.player1, "不能加入自己创建的游戏");
        require(msg.value == game.stakeAmount, "押金金额不匹配");

        game.player2 = msg.sender;
        game.status = GameStatus.InProgress;

        emit GameJoined(gameId, msg.sender);
    }

    // 落子逻辑
    function makeMove(uint256 gameId, uint8 position) external {
        Game storage game = games[gameId];
        require(game.status == GameStatus.InProgress, "游戏未进行中");
        require(position < 9, "无效位置");
        require(game.board[position] == 0, "位置已被占用");
        require(msg.sender == game.player1 || msg.sender == game.player2, "你不是游戏玩家");

        game.board[position] = (msg.sender == game.player1) ? 1 : 2;
        checkGameResult(gameId);
    }

    // 检查游戏结果并处理结算
    function checkGameResult(uint256 gameId) internal {
        Game storage game = games[gameId];
        uint8[9] memory board = game.board;

        // 胜负判定组合:行、列、对角线
        uint8[8][3] memory winningCombos = [
            [0,1,2], [3,4,5], [6,7,8],
            [0,3,6], [1,4,7], [2,5,8],
            [0,4,8], [2,4,6]
        ];

        for (uint i = 0; i < winningCombos.length; i++) {
            uint8 a = board[winningCombos[i][0]];
            uint8 b = board[winningCombos[i][1]];
            uint8 c = board[winningCombos[i][2]];
            if (a != 0 && a == b && b == c) {
                game.winner = (a == 1) ? game.player1 : game.player2;
                game.status = GameStatus.Completed;
                payoutWinner(gameId);
                emit GameEnded(gameId, game.winner);
                return;
            }
        }

        // 平局判定:棋盘已满
        bool isBoardFull = true;
        for (uint i = 0; i < board.length; i++) {
            if (board[i] == 0) {
                isBoardFull = false;
                break;
            }
        }
        if (isBoardFull) {
            game.status = GameStatus.Completed;
            refundBothPlayers(gameId);
            emit GameEnded(gameId, address(0)); // 0地址表示平局
        }
    }

    // 向获胜者转账全部押金
    function payoutWinner(uint256 gameId) internal {
        Game storage game = games[gameId];
        uint256 totalStake = game.stakeAmount * 2;
        payable(game.winner).transfer(totalStake);
    }

    // 平局时退还双方押金
    function refundBothPlayers(uint256 gameId) internal {
        Game storage game = games[gameId];
        payable(game.player1).transfer(game.stakeAmount);
        payable(game.player2).transfer(game.stakeAmount);
    }

    // 对手超时未加入时,创建者可撤回押金
    function withdrawStakeIfWaiting(uint256 gameId) external {
        Game storage game = games[gameId];
        require(game.status == GameStatus.WaitingForOpponent, "游戏不处于等待状态");
        require(msg.sender == game.player1, "仅创建者可撤回");

        payable(msg.sender).transfer(game.stakeAmount);
        delete games[gameId];
    }
}

2. ERC20代币版本合约

如果使用自定义ERC20代币,需修改为代币授权+转移的模式:

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TicTacToeERC20 {
    IERC20 public immutable token;

    enum GameStatus { WaitingForOpponent, InProgress, Completed }
    struct Game {
        address player1;
        address player2;
        uint256 stakeAmount;
        GameStatus status;
        address winner;
        uint8[9] board;
    }

    mapping(uint256 => Game) public games;
    uint256 public nextGameId;

    event GameCreated(uint256 gameId, address creator, uint256 stake);
    event GameJoined(uint256 gameId, address player2);
    event GameEnded(uint256 gameId, address winner);

    constructor(address _tokenAddress) {
        token = IERC20(_tokenAddress);
    }

    // 创建游戏:用户需先授权合约,再转移代币到合约锁定
    function createGame(uint256 stakeAmount) external {
        require(stakeAmount > 0, "押金金额不能为0");
        require(token.transferFrom(msg.sender, address(this), stakeAmount), "代币转移失败");
        
        uint256 gameId = nextGameId++;
        games[gameId] = Game({
            player1: msg.sender,
            player2: address(0),
            stakeAmount: stakeAmount,
            status: GameStatus.WaitingForOpponent,
            winner: address(0),
            board: [0,0,0,0,0,0,0,0,0]
        });

        emit GameCreated(gameId, msg.sender, stakeAmount);
    }

    // 加入游戏:对手需存入等额代币
    function joinGame(uint256 gameId) external {
        Game storage game = games[gameId];
        require(game.status == GameStatus.WaitingForOpponent, "游戏不处于等待对手状态");
        require(msg.sender != game.player1, "不能加入自己创建的游戏");
        require(token.transferFrom(msg.sender, address(this), game.stakeAmount), "代币转移失败");

        game.player2 = msg.sender;
        game.status = GameStatus.InProgress;

        emit GameJoined(gameId, msg.sender);
    }

    // 获胜者结算(代币版)
    function payoutWinner(uint256 gameId) internal {
        Game storage game = games[gameId];
        uint256 totalStake = game.stakeAmount * 2;
        require(token.transfer(game.winner, totalStake), "代币发放失败");
    }

    // 平局退款(代币版)
    function refundBothPlayers(uint256 gameId) internal {
        Game storage game = games[gameId];
        require(token.transfer(game.player1, game.stakeAmount), "退款失败");
        require(token.transfer(game.player2, game.stakeAmount), "退款失败");
    }

    // 其他逻辑(落子、胜负判定、超时撤回)和ETH版本一致,此处省略
}

二、Web3.js前端交互实现

1. 连接MetaMask钱包

async function connectWallet() {
    if (!window.ethereum) {
        alert("请安装MetaMask钱包");
        return null;
    }
    try {
        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        return accounts[0];
    } catch (error) {
        console.error("钱包连接失败:", error);
        return null;
    }
}

2. 创建ETH版本游戏

async function createETHGame(contractInstance, stakeEthAmount) {
    const account = await connectWallet();
    if (!account) return;

    const stakeWei = web3.utils.toWei(stakeEthAmount.toString(), 'ether');
    try {
        const tx = await contractInstance.methods.createGame().send({
            from: account,
            value: stakeWei
        });
        const gameId = tx.events.GameCreated.returnValues.gameId;
        console.log("游戏创建成功,ID:", gameId);
        // 可将gameId和合约地址分享给好友
    } catch (error) {
        console.error("创建游戏失败:", error);
    }
}

3. 加入ETH版本游戏

async function joinETHGame(contractInstance, gameId, stakeEthAmount) {
    const account = await connectWallet();
    if (!account) return;

    const stakeWei = web3.utils.toWei(stakeEthAmount.toString(), 'ether');
    try {
        await contractInstance.methods.joinGame(gameId).send({
            from: account,
            value: stakeWei
        });
        console.log("成功加入游戏");
    } catch (error) {
        console.error("加入游戏失败:", error);
    }
}

4. ERC20版本游戏交互(需先授权)

// 授权合约花费代币
async function approveERC20(tokenContract, gameContractAddress, stakeAmount) {
    const account = await connectWallet();
    if (!account) return;

    const amount = web3.utils.toWei(stakeAmount.toString(), 'ether');
    try {
        await tokenContract.methods.approve(gameContractAddress, amount).send({ from: account });
        console.log("代币授权成功");
    } catch (error) {
        console.error("授权失败:", error);
    }
}

// 创建ERC20版本游戏
async function createERC20Game(gameContract, tokenContract, stakeAmount) {
    await approveERC20(tokenContract, gameContract.options.address, stakeAmount);
    const account = await connectWallet();
    try {
        const tx = await gameContract.methods.createGame(web3.utils.toWei(stakeAmount.toString(), 'ether')).send({ from: account });
        const gameId = tx.events.GameCreated.returnValues.gameId;
        console.log("ERC20游戏创建成功,ID:", gameId);
    } catch (error) {
        console.error("创建游戏失败:", error);
    }
}

三、关键注意事项

  • 安全防护:Solidity 0.8+默认开启溢出检查,使用transfer(ETH)或OpenZeppelin的安全转账函数(ERC20)避免重入攻击;
  • 状态同步:前端需监听合约的GameCreated、GameJoined、GameEnded事件,实时更新UI状态;
  • 超时机制:建议添加时间限制,比如创建游戏后24小时对手未加入,允许创建者撤回押金;
  • 本地验证:前端可做棋盘状态的本地校验,但最终结果以合约执行为准,防止用户作弊。

内容的提问来源于stack exchange,提问作者webflax developers

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 21:54:54