# 基于 sCrypt 合约开发一个完整的 dApp：井字棋游戏

50 篇文章 2 订阅
79 篇文章 6 订阅

1. 编写合约
2. 测试合约
3. 集成一个简单的 Web 应用程序与合约进行交互

## 搭建开发环境

1. 安装 sCrypt IDE，见 sCrypt 开发工具篇 - Visual Studio Code 插件
2. 安装 nodejs, version >= 12
3. 安装 Typescript

## 使用 sCrypt 编写 tic-tac-toe 合约

TicTacToe 合约主要实现原理是通过有状态合约将游戏的状态存储在合约中。井字棋游戏状态由以下组成：

1. turn : 布尔类型。表示轮到谁下棋, true 表示轮到 Alice， false 表示轮到 Bob
2. board : 整数数组类型。记录棋盘当前的状态，每个元素代表棋盘的一个位置，0 表示没有棋子，1 表示 ALICE的棋子，2 表示BOB的棋子，长度为 9


contract TicTacToe {
PubKey alice;
PubKey bob;

// if it is alice's turn to play
@state
bool isAliceTurn;

// state of the board. For example, a board with Alice in the first row
// and first column is expressed as [1,0,0,0,0,0,0,0,0]
@state
int[N] board;

static const int N = 9;
static const int EMPTY = 0;
static const int ALICE = 1;
static const int BOB = 2;

public function move(int n, Sig sig, int amount, SigHashPreimage txPreimage) {

require(Tx.checkPreimage(txPreimage));
require(n >= 0 && n < N);

// not filled
require(this.board[n] == EMPTY);

int play = this.isAliceTurn ? ALICE : BOB;
PubKey player = this.isAliceTurn ? this.alice : this.bob;

// ensure it's player's turn
require(checkSig(sig, player));
// make the move
this.board[n] = play;
this.isAliceTurn = !this.isAliceTurn;

bytes outputs = b'';
if (this.won(play)) {
bytes outputScript = Utils.buildPublicKeyHashScript(hash160(player));
bytes output = Utils.buildOutput(outputScript, amount);
outputs = output;
}
else if (this.full()) {
bytes aliceScript = Utils.buildPublicKeyHashScript(hash160(this.alice));
bytes aliceOutput = Utils.buildOutput(aliceScript, amount);

bytes bobScript = Utils.buildPublicKeyHashScript(hash160(this.bob));
bytes bobOutput = Utils.buildOutput(bobScript, amount);

outputs = aliceOutput + bobOutput;
}
else {
bytes scriptCode_ = this.getStateScript();
bytes output = Utils.buildOutput(scriptCode_, amount);
outputs = output;
}

require(hash256(outputs) == SigHash.hashOutputs(txPreimage));
}

function won(int play) : bool {
// three in a row, a column, or a diagonal
int[8][3] lines = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];

bool anyLine = false;
loop (8) : i {
bool line = true;
loop (3) : j {
line = line && this.board[lines[i][j]] == play;
}

anyLine = anyLine || line;
}

return anyLine;
}

function full() : bool {
bool full = true;

loop (N) : i {
full = full && this.board[i] != TicTacToe.EMPTY;
}

return full;
}
}

TicTacToe 合约源代码

### 游戏规则

1. 如果玩家获胜，他/她将拿走合约中锁定的所有资金
2. 如果棋盘已满且无人获胜，则为平局，Alice 和 Bob 各拿一半的资金
3. 否则，游戏仍在进行中，下一个玩家移动棋子

## scryptlib

dApp 需要在前端页面与合约进行交互。 要做到这一点，我们将使用 sCrypt 官方发布的 JavaScript 库 —— scryptlib.

scryptlib 是用于集成以 sCrypt 语言编写的 Bitcoin SV 智能合约的 Javascript/TypeScript SDK。

## scryptlib 安装

scryptlib 可以通过 npm 安装。

// use NPM
npm install scryptlib

// use Yarn


const Demo = buildContractClass(compileContract('demo.scrypt'));
const demo = new Demo(7, 4);

assert(result.success);


## 测试合约

it('One full round where Alice wins', () => {

// Alice places an X at 0-th cell
testMove(true, 0, moveScript(false, [1,0,0,0,0,0,0,0,0]))

// Bob places an O at 4-th cell
testMove(false, 4, moveScript(true, [1,0,0,0,2,0,0,0,0]))

// Alice places an X at 1-th cell
testMove(true, 1, moveScript(false, [1,1,0,0,2,0,0,0,0]))

// Bob places an O at 8-th cell
testMove(false, 8, moveScript(true, [1,1,0,0,2,0,0,0,2]))

// Alice places an X at 2-th cell and wins
});

tictactoe.scrypttest.js

## 集成 Web App

### 编译合约

1. 通过 IDE 将 TicTacToe 进行编译，得到合约描述文件 tictactoe_release_desc.json。并将其拷贝到 public 目录中，以便我们的能从前端页面加载到该文件。

2. 在前端页面使用 fetchContract 函数加载合约描述文件 tictactoe_release_desc.json，并实例化合约对象。将合约对象保存在状态中。

async function fetchContract(alicePubKey, bobPubKey) {
let { contractClass: TictactoeContractClass } = await web3.loadContract(
"/tic-tac-toe/tictactoe_release_desc.json"
);

return new TictactoeContractClass(
new PubKey(alicePubKey),
new PubKey(bobPubKey),
true,
[0,0,0,0,0,0,0,0,0]
);
}

...

const instance = await fetchContract(PlayerPublicKey.get(Player.Alice),
PlayerPublicKey.get(Player.Bob))

...
instance: instance
})



### 集成钱包

#### 2. 钱包初始化

App 加载时， 使用 useEffect 来初始化钱包。首先，为 web3 设置一个 SensiletWallet 钱包。然后调用 web3.wallet.isConnected() 将钱包是否连接的状态保存起来。

useEffect(async () => {
const timer = setTimeout(async ()=> {
//set a SensiletWallet for web3
web3.setWallet(new SensiletWallet());
const isConnected = await web3.wallet.isConnected();

...

...
isConnected: isConnected,
})

}, 100)

return () => {
clearTimeout(timer)
}
}, []);


App 的渲染代码中，通过判断 states.isConnected 状态来决定渲染钱包登入组件 Auth 还是钱包余额组件 Balance

return (
<div className="App">
<h2>Play Tic-Tac-Toe on Bitcoin</h2>
...
{states.isConnected ? <Balance></Balance> : <Auth></Auth>}
</div>
);


#### 3. 钱包登入

import { web3 } from "./web3";

const Auth = (props) => {

const sensiletLogin = async (e) => {
try {
const res = await web3.wallet.requestAccount("tic-tac-toe");
if (res) {
}
} catch (error) {
console.error("requestAccount error", error);
}
};

return (
<div className="auth">
<div>
<button
className="pure-button button-large sensilet"
>
Sensilet
</button>
</div>
</div>
);
};

export default Auth;


#### 4. 钱包余额

Balance 组件调用了钱包的 getbalance 接口，实现了展示钱包余额的功能。

import { useState, useEffect } from "react";
import { web3 } from "./web3";
const Balance = (props) => {
const [balance, setBalance] = useState(0);

useEffect(async () => {
if (web3.wallet) {
web3.wallet.getbalance().then((balance) => {
setBalance(balance);
});
}
}, []);

return (
<div className="wallet">
<div className="walletInfo">
<div className="balance">
<label>Balance: {balance} <span> (satoshis)</span></label>
</div>
</div>
</div>
);
};

export default Balance;


### 部署合约

const startGame = async (amount) => {

if (web3.wallet && states.instance) {

web3.deploy(states.instance, amount).then(rawTx => {
//initial game states
let gameStates = {
amount: amount,
name: "tic-tac-toe",
date: new Date(),
history: [
{
squares: Array(9).fill(null),
},
],
currentStepNumber: 0,
isAliceTurn: true,
};

//save utxo
//save Game data
GameData.set(gameStates);
//first player is Alice
CurrentPlayer.set(Player.Alice);
//update states.started
started: true
}))
})
}
};


1. 调用钱包的 listUnspent 接口，查询可用的 UTXO 来支付部署交易的费用。
2. 使用链式 APIs 构建包含合约实例 contract 的交易
3. 调用钱包的 signRawTransaction 接口对于交易进行签名
4. 最后调用 web3.sendRawTx 广播交易

static async deploy(contract: AbstractContract, amountInContract: number): Promise<string> {
const wallet = web3.wallet

return wallet.listUnspent(amountInContract, {
purpose: 'tic-tac-toe'
}).then((utxos: UTXO[]) => {
const tx = new bsv.Transaction();
tx.from([utxos[0]])
script: contract.lockingScript,
satoshis: amountInContract,
}))

return wallet.signRawTransaction(tx.toString(), utxos[0].script, utxos[0].satoshis, 0, SignType.ALL);
}).then(async (rawTx: string) => {
await web3.sendRawTx(rawTx);
return rawTx;
})
}


## 调用合约

web3.call() 第一个参数是包含合约实例的 UTXO，作为构建调用合约的交易的第一个输入。第二个参数是一个回调函数。我们在

1. 从存储中取出包含合约实例的最新的 UTXO。作为交易的输入。
2. 根据游戏的状态和游戏规则来给交易添加输出。添加输出的过程中， 使用 toContractState() 函数将游戏状态转换成合约状态。

let winner = calculateWinner(squares).winner;

if (winner) { // Current Player won

tx.setOutput(0, (tx) => {
return new bsv.Transaction.Output({
satoshis: contractUtxo.satoshis - tx.getEstimateFee(),
})
})

} else if (history.length >= 9) { //board is full

tx.setOutput(0, (tx) => {
return new bsv.Transaction.Output({
satoshis: (contractUtxo.satoshis - tx.getEstimateFee()) /2,
})
})
.setOutput(1, (tx) => {
return new bsv.Transaction.Output({
satoshis: (contractUtxo.satoshis - tx.getEstimateFee()) /2,
})
})

} else { //continue move

const newStates = toContractState(gameState);
const newLockingScript = this.props.contractInstance.getNewStateScript(newStates);
tx.setOutput(0, (tx) => {
const amount = contractUtxo.satoshis - tx.getEstimateFee();
return new bsv.Transaction.Output({
script: newLockingScript,
satoshis: amount,
})
})
}

1. 设置合约解锁脚本。
tx.setInputScript(0, (tx, output) => {

const preimage = getPreimage(tx, output.script, output.satoshis)
const privateKey = new bsv.PrivateKey.fromWIF(PlayerPrivkey.get(CurrentPlayer.get()));
const sig = signTx(tx, privateKey, output.script, output.satoshis)
const amount = contractUtxo.satoshis - tx.getEstimateFee();

return this.props.contractInstance.move(i, sig, amount, preimage).toScript();
})
.seal()

1. 使用钱包提供的 sendRawTransaction 接口广播交易。这封装在 web3.call() 中。

2. 广播成功后，需要保存调用的交易和包含合约实例的UTXO, 作为下一次调用的输入。 同时还需要更新游戏状态和合约实例的状态。

const utxo = ContractUtxos.add(rawTx); // save latest utxo
GameData.update(gameState); //update game's states
this.attachState(); //update stateful contract's states


# 总结

[1]: 本文演示的游戏可以在 这里 试玩

[2]: 本文使用的所有代码均源自这个 Github Repo ，欢迎大家加星收藏。

09-25 1579
06-04 1万+
08-29 271
01-02 1034
01-11 6735

### “相关推荐”对你有帮助么？

• 非常没帮助
• 没帮助
• 一般
• 有帮助
• 非常有帮助

sCrypt 智能合约

¥2 ¥4 ¥6 ¥10 ¥20

1.余额是钱包充值的虚拟货币，按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载，可以购买VIP、C币套餐、付费专栏及课程。