在我們之前的博客中,介紹了如何使用 sCrypt 來撰寫位元幣智能合約,但是作為剛入門的開發者,你可能對如何使用 sCrypt 來構建 dApp 更加感興趣,接下來我們將教大家如何使用 sCrypt 一步一步地構建一個井字棋 dApp.
該應用程式非常簡單,它所做的就是使用兩個玩家(分別是 Alice 和 Bob)的公鑰哈希,初始化合約,各自下注相同的金額鎖定到合約中,只有贏得那個人可以取走合約里面的錢,如果最后沒有人贏,則兩個玩家各自可以取走一半的錢,目標不僅是對應用程式進行編碼,主要是學習如何對其進行編譯,測驗,部署和互動的程序,
我們將逐步完成構建全堆疊去中心化應用程式的整個程序,包括:
- 撰寫合約
- 測驗合約
- 將 web app 集成合約
搭建開發環境
- 安裝 sCrypt IDE,見 sCrypt 開發工具篇 - Visual Studio Code 插件
- 安裝 nodejs, version >= 12
- 安裝 Typescript
搭建開發環境非常簡單方便,接下來我們用 create-react-app 來創建一個 react app, 執行 npx create-react-app tic-tac-toe,然后用 vscode 打開我們剛剛創建的代碼工程,并在根目錄下創建一個contracts 目錄,用來存放我們的合約代碼,創建一個 test 目錄,用來存放合約的測驗代碼,你將看到以下目錄結構,

使用 sCrypt 撰寫 tic-tac-toe 合約
我們將使用 sCrypt 編程語言來撰寫一個名為 TicTacToe 的合約, TicTacToe 合約主要實作原理是將游戲的狀態存盤在合約中,這在之前的文章已經詳細介紹過了
游戲狀態由以下組成:
turn: 輪到誰下棋, 0 表示輪到 Alice, 1 表示輪到 Bob, 長度為 1 byteboard: 記錄棋盤當前的狀態,每個位元組代表棋盤的一個位置,0 表示空,1 表示 ALICE,2 表示 BOB,長度為 9 byte
import "util.scrypt";
contract TicTacToe {
PubKey alice;
PubKey bob;
static const int TURNLEN = 1;
static const int BOARDLEN = 9;
static const bytes EMPTY = b'00';
static const bytes ALICE = b'01';
static const bytes BOB = b'02';
public function move(int n, Sig sig, int amount, SigHashPreimage txPreimage) {
require(Tx.checkPreimage(txPreimage));
require(n >= 0 && n < BOARDLEN);
bytes scriptCode = Util.scriptCode(txPreimage);
int scriptLen = len(scriptCode);
int boardStart = scriptLen - BOARDLEN;
// state: turn (1 byte) + board (9 bytes)
int turn = unpack(scriptCode[boardStart - TURNLEN : boardStart]);
bytes board = scriptCode[boardStart : ];
// not filled
require(Util.getElemAt(board, n) == EMPTY);
bytes play = turn == 0 ? ALICE : BOB;
PubKey player = turn == 0 ? this.alice : this.bob;
// ensure it's player's turn
require(checkSig(sig, player));
// make the move
board = Util.setElemAt(board, n, play);
bytes outputs = b'';
if (this.won(board, play)) {
// winner takes all
bytes outputScript = Util.pubKeyToP2PKH(player);
bytes output = Util.buildOutput(outputScript, amount);
outputs = output;
}
else if (this.full(board)) {
// draw: equally split, i.e., both outputs have the same amount
bytes aliceScript = Util.pubKeyToP2PKH(this.alice);
bytes aliceOutput = Util.buildOutput(aliceScript, amount);
bytes bobScript = Util.pubKeyToP2PKH(this.bob);
bytes bobOutput = Util.buildOutput(bobScript, amount);
outputs = aliceOutput + bobOutput;
} else {
// update state: next turn & next board
bytes scriptCode_ = scriptCode[ : scriptLen - BOARDLEN - TURNLEN] + num2bin(1 - turn, TURNLEN) + board;
bytes output = Util.buildOutput(scriptCode_, amount);
outputs = output;
}
require(hash256(outputs) == Util.hashOutputs(txPreimage));
}
// does play win after current move?
function won(bytes board, bytes play) : bool {
// three in a row, a column, or a diagnoal
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 && Util.getElemAt(board, lines[i][j]) == play;
}
anyLine = anyLine || line;
}
return anyLine;
}
// is board full?
function full(bytes board) : bool {
bool full = true;
loop (BOARDLEN) : i {
full = full && Util.getElemAt(board, i) != EMPTY;
}
return full;
}
}
合約中有 3 個函式:
move愛麗絲(Alice)和鮑勃(Bob)各自將 X 個位元幣鎖定在包含上述合同的一個 UTXO 中, 接下來,他們通過呼叫公共函式move()交替玩游戲won檢查是否有玩家已經贏得比賽,他將能取走所有合約鎖定的賭注full如果棋盤,沒人贏得比賽,則兩個人平分賭注
測驗合約
接下來我們用 Javascript 撰寫合約的單元測驗,以確保合約在上線部署之前能夠按預期作業, 通過sCrypt 測驗框架,我們可以模擬呼叫 move() 并斷言游戲狀態
const Tictactoe = buildContractClass(runCompile('tictactoe.scrypt'));
game = new Tictactoe(new PubKey(toHex(publicKey1)), new PubKey(toHex(publicKey2)));
let state = new Bytes('00000000000000000000').toASM();
game.setDataPart(state)
describe('Test sCrypt contract Tictactoe In Javascript', () => {
let result, preimage, sig, prevLockingScript
it('Alice places an X at 0-th cell', () => {
prevLockingScript = game.lockingScript.toASM();
let newState = new Bytes('01010000000000000000').toASM();
const tx = newTx();
const newLockingScript = [game.codePart.toASM(), newState].join(' ');
tx.addOutput(new bsv.Transaction.Output({
script: bsv.Script.fromASM(newLockingScript),
satoshis: 10000
}))
preimage = getPreimage(tx, prevLockingScript, inputSatoshis);
sig = signTx(tx, privateKey1, prevLockingScript, inputSatoshis)
const context = { tx, inputIndex, inputSatoshis }
result = game.move(0, new Sig(toHex(sig)), 10000, preimage).verify(context)
expect(result.success, result.error).to.be.true;
game.setDataPart(newState)
});
}
集成合約
1. 部署合約
我們將復用 官方 React 教程 中現有的 tic-tac-toe 專案,如果您有前端開發的經驗,這應該看起來很熟悉,我們將專注于集成 dApp 的智能合約部分,對于與位元幣區塊鏈的所有互動,我們使用 whatsonchain 提供的 API,
-
通過右鍵單擊 編譯 來編譯我們的合約,將會輸出
tictactoe_desc.json,這包含有關我們合約的所有內容,將其拷貝到public目錄中,以便我們的能從前端頁面加載到該檔案,

-
我們需要先實作一個測驗網的錢包,我們將錢包的介面定義在wallet.ts 中, 包括以下介面:
//Dapp use this api to connect to the wallet. abstract requestAccount(name: string, permissions: string[]): Promise<Account>; //get wallet balance abstract getbalance(): Promise<number>; //sign raw transaction, returns unlockscript of the p2pkh input if success abstract signRawTransaction(tx: Tx, inputIndex: number, sigHashType: SignType ): Promise<string>; //get signature for special input abstract getSignature(tx: Tx, inputIndex: number, sigHashType: SignType ): Promise<string>; //send raw transaction, returns transaction hash if success abstract sendRawTransaction(rawTx: string): Promise<string>; //returns array of unspent transaction outputs, which total amount is more than the minAmount argument. abstract listUnspent(minAmount: number, options?: { purpose?: string }): Promise<UTXO[]>; //returns a new Bitcoin address, for receiving change. abstract getRawChangeAddress(options?: { purpose?: string }): Promise<string>; //returns a public key abstract getPublicKey(options?: { purpose?: string }): Promise<string>;localwallet.ts 則是我們的具體實作,
我們創建 wallet.js react 組件,并在頁面中繪制出來

這樣 Bob 和 Alice 就能往 dApp 充位元幣了,
-
資金準備就緒后,就能使用步驟 1 中的
tictactoe_desc.json以及 Alice 和 Bob 的公鑰來實體化合約了,async function fetchContract(alicePubKey, bobPubKey) { let { contractClass: TictactoeContractClass } = await web3.loadContract( "/tic-tac-toe/tictactoe_desc.json" ); let instance = newCall(TictactoeContractClass, [ new PubKey(toHex(alicePubKey)), new PubKey(toHex(bobPubKey)), ]); instance.setDataPart("00000000000000000000"); updateContractInstance(instance); console.log("fetchContract successfully"); return instance; } -
合約實體化后,可以通過合約實體來構建交易了,Alice 和 Bob 分別提供一個 Input,同時我們給他添加一個對應的 Output 用于找零,最后我們構建出來一個包含 2 個 Input 和 3 個 Output 的交易,
static async buildDeployTx(contract: AbstractContract, amountInContract: number, alicePrivateKey: string, bobPrivateKey: string): Promise<Tx> { let aliceWallet = new LocalWallet(NetWork.Testnet, alicePrivateKey); let bobWallet = new LocalWallet(NetWork.Testnet, bobPrivateKey); const aliceChangeAddress = await aliceWallet.getRawChangeAddress(); const bobChangeAddress = await bobWallet.getRawChangeAddress(); const tx: Tx = { inputs: [], outputs: [] }; tx.outputs.push({ script: contract.lockingScript.toHex(), satoshis: amountInContract * 2 }); const minAmount = amountInContract + FEE; return aliceWallet.listUnspent(minAmount, { purpose: 'change' }).then(async (utxos: UTXO[]) => { if (utxos.length === 0) { throw new Error('no utxos'); } //add input which using utxo from alice tx.inputs.push( { utxo: utxos[0], script: '', sequence: 0 } ); const changeAmount = utxos[0].satoshis - amountInContract - FEE; if (changeAmount <= 0) { throw new Error('fund is not enough'); } //add alice change output tx.outputs.push( { script: bsv.Script.buildPublicKeyHashOut(aliceChangeAddress).toHex(), satoshis: changeAmount } ); return tx; }).then(tx => { return bobWallet.listUnspent(minAmount, { purpose: 'change' }).then(async (utxos: UTXO[]) => { if (utxos.length === 0) { throw new Error('no utxos'); } //add input which using utxo from bob tx.inputs.push( { utxo: utxos[0], script: '', sequence: 0 } ); const changeAmount = utxos[0].satoshis - amountInContract - FEE; if (changeAmount <= 0) { throw new Error('fund is not enough'); } //add bob change output tx.outputs.push( { script: bsv.Script.buildPublicKeyHashOut(bobChangeAddress).toHex(), satoshis: changeAmount } ); return tx; }) }).then(tx => { //alice sign return aliceWallet.signRawTransaction(tx, 0, SignType.ALL).then(unlockscript => { tx.inputs[0].script = unlockscript; return tx; }) }).then(tx => { //bob sign return bobWallet.signRawTransaction(tx, 1, SignType.ALL).then(unlockscript => { tx.inputs[1].script = unlockscript; return tx; }) }) } -
交易構建完成后,分別由 Alice 和 Bob 進行簽名,然后廣播,從而完成合約的部署,我們嘗試運行
npm start,并用瀏覽器打開http://localhost:3000, 嘗試開始游戲,如果順利,我們將會看到部署成功的交易,嘗試打開瀏覽一下交易:

2. 呼叫合約
接下來就是開始下棋了,每下一步棋,就是對合約的一次呼叫,并觸發合約狀態的改變,呼叫合約需要構建一個符合合約規則的交易,首先需要計算合約的新狀態,同時,我們可以在 dApp 通過棋盤狀態推演出來誰輸誰贏,這樣就能根據結果來構建交易的輸出,下面就是推演結果和構建呼叫合約交易的代碼,
-
計算合約的新狀態
calculateNewState(squares) { return (!this.state.xIsNext ? '00' : '01') + squares.map(square => { if (square && square.label === 'X') { return '01' } else if (square && square.label === 'O') { return '02' } else { return '00'; } }).join(''); } -
構建呼叫合約的交易
async buildCallContractTx(i, newState, squares, history) { let newLockingScript = ""; let winner = calculateWinner(squares).winner; const FEE = 3000; let outputs = []; let amount = this.props.game.lastUtxo.satoshis - FEE; if (winner) { // winner is current player let address = await web3.wallet.getRawChangeAddress(); newLockingScript = bsv.Script.buildPublicKeyHashOut(address).toHex(); outputs.push({ satoshis: amount, script: newLockingScript }) } else if (history.length >= 9) { const aliceAddress = new bsv.PublicKey(this.props.game.alicePubKey, { network: bsv.Networks.testnet }); const bobAddress = new bsv.PublicKey(this.props.game.bobPubKey, { network: bsv.Networks.testnet }); //no body win const aliceLockingScript = bsv.Script.buildPublicKeyHashOut(aliceAddress.toAddress(bsv.Networks.testnet)).toHex(); const bobLockingScript = bsv.Script.buildPublicKeyHashOut(bobAddress.toAddress(bsv.Networks.testnet)).toHex(); amount = (this.props.game.lastUtxo.satoshis - FEE) / 2; outputs.push({ satoshis: amount, script: aliceLockingScript }) outputs.push({ satoshis: amount, script: bobLockingScript }) } else { //next newLockingScript = [this.props.contractInstance.codePart.toHex(), bsv.Script.fromASM(newState).toHex()].join(''); outputs.push({ satoshis: amount, script: newLockingScript }) } if (outputs[0].satoshis <= 0) { alert(`fund in contract is too low `) return undefined; } let tx = { inputs: [{ utxo: this.props.game.lastUtxo, sequence: 0, script: "" }], outputs: outputs } let preimage = getPreimage(tx); let sig = await web3.wallet.getSignature(tx, 0, SignType.ALL, true); let unlockScript = this.props.contractInstance.move(i, new Sig(toHex(sig)), amount, preimage).toHex(); tx.inputs[0].script = unlockScript; return tx; }上面 Input 對應的
script默認是空的,也就是構建的交易并沒包含解鎖腳本,我們需要計算并填充解鎖腳本,才能構成一個完整的交易, 我們知道 sCrypt 合約 的public方法的引數就是對應的解鎖腳本,TicTacToe合約的 move 函式有 4 個引數:n棋盤位置amount合約花費后剩下的余額txPreimage交易原象, 如果您對這個引數不了解,可以查看深入學習位元幣腳本之 OP_PUSH_TXsig對交易的簽名
前面 2 個參我們都可以在 dApp 端計算,
txPreimage由于未簽名的交易模板已經構建好,我們也可以直接在 dApp 端直接呼叫getPreimage方法計算,sig的計算涉及到私鑰,我需要使用錢包的getSignature方法,當 4 個解鎖引數我們都計算好之后,我們可以呼叫合約實體的
move方法來組裝解鎖腳本得到unlockScript... let unlockScript = this.props.contractInstance.move(i, new Sig(toHex(sig)), amount, preimage).toHex(); tx.inputs[0].script = unlockScript; // 填充解鎖腳本后,交易才算是構建完整了 ... -
交易構建完成了,接下來通過 API 將交易廣播到區塊鏈上,同時我們更新合約最新的 UTXO, 以便下次呼叫合約時候可以直接使用,
web3.sendTx(tx).then(txid => { squares[i].tx = txid; squares[i].n = history.length; let gameState = { history: history.concat([ { squares, currentLocation: getLocation(i), stepNumber: history.length, }, ]), xIsNext: !this.state.xIsNext, currentStepNumber: history.length, }; server.saveGame(Object.assign({}, this.props.game, { gameState: gameState, lastUtxo: { txHash: txid, outputIndex: 0, satoshis: tx.outputs[0].satoshis, script: tx.outputs[0].script } }), 'next') this.setState(gameState); }).catch(e => { ... })注意,由于這里我們使用的 localstorage 來模擬服務器通信,所以 alice 更新 UTXO 后,bob 也就能獲取到對應的最新的 UTXO,在實際的生產環境中,合約的最新的 utxo 需要通過其它方法來獲取,
至此,我們完成了 TicTacToe 小游戲下棋動作和合約呼叫的系結,玩家的每個下棋動作,都產生一個區塊鏈上對應的 transaction 與之對應,
總結
恭喜你! 您剛剛在位元幣上構建了第一個全堆疊 dApp, 現在,您可以玩井字游戲或在位元幣上構建您自己喜歡的游戲,現在是時候喝些香檳了,或者打開下方連接和小伙伴來一場比賽!
- 本文演示的游戲可以在 這里試玩(注意僅支持測驗網,請勿使用主網上的錢包)
- 本文使用的所有代碼均源自這個 Github Repo ,歡迎大家加星收藏,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/272297.html
標籤:區塊鏈
