Tic Tac Toe On Bitcoin SV Blockchain
Bitcoin SV (BSV) blockchain upholds the original vision of Bitcoin as outlined by Satoshi Nakamoto in the bitcoin whitepaper. Bitcoin SV distinguishes itself by providing a stable and scalable foundation for building dApps, leveraging it robust infrastructure and unique features.
In this comprehensive guide, we will delve into building a decentralized Tic-Tac-Toe game on the Bitcoin SV blockchain using sCrypt. From setting up the development environment to writing the smartcontracts and frontend integration, we will provide step-by-step instructions, practical insights to help you navigate the exciting world of Bitcoin SV dApp development using sCrypt.
The game is initialized with the Bitcoin public key of two players (Alice and Bob respectively). They each bet the same amount and lock it into the contract. The winner takes all bitcoins locked in the contract. If no one wins and there is a draw, the two players can each withdraw half of the money.
sCrypt Overview
sCrypt
is an embedded Domain Specific Language
(eDSL) based on TypeScript for writing smart contracts on Bitcoin SV. sCrypt simplifies the process of writing, testing, and deploying smart contracts, empowering developers to build secure and scalable applications on the Bitcoin SV network.
Prerequisite to Get Started
Make sure all prerequisite are install;
Install
Node.js
(require version>=16
) andNPM
on your machine by following the instructions over hereInstall Git
Next, install the sCrypt Cli: follow this command to install the sCrypt Cli globally to your machine
npm install -g scrypt-cli
The tools we will be using for this project include;
sCrypt language: a typescript base framework for writing smartcontract on Bitcoin SV with a high-level abstraction. It’s a static –typed language which provides a safety and easy to use.
sCrypt library: a comprehensive library for client-side seamless interaction with the Bsv blockchain
sCrypt Cli: to easily create and compile sCrypt into bitcoin script and provides a best-practice scaffolding to help developers follow the recommended standards and conventions for writing sCrypt code.
React: the client-side framework.
Yours wallet:Yours Wallet is an open-source, non-custodial Bitcoin SV web3 wallet designed for ease of use and everyday transactions. Yours offers a straightforward and secure way to receive, send, and manage your digital assets.
Getting Started
create a new React project with TypeScript template
npx create-react-app tic tac toe --template typescript
add sCrypt dependencies to your project.
cd tic-tac-toe
npx scrypt-cli@latest init
Creating Tic Tac Toe Contract
create a contract at src/contracts/tictactoe.ts
import {
prop, method, SmartContract, PubKey, FixedArray, assert, Sig, Utils, toByteString, hash160,
hash256,
fill,
ContractTransaction,
MethodCallOptions,
bsv
} from "scrypt-ts";
export class TicTacToe extends SmartContract {
@prop()
alice: PubKey;
@prop()
bob: PubKey;
@prop(true)
isAliceTurn: boolean;
@prop(true)
board: FixedArray<bigint, 9>;
static readonly EMPTY: bigint = 0n;
static readonly ALICE: bigint = 1n;
static readonly BOB: bigint = 2n;
constructor(alice: PubKey, bob: PubKey) {
super(...arguments)
this.alice = alice;
this.bob = bob;
this.isAliceTurn = true;
this.board = fill(TicTacToe.EMPTY, 9);
}
@method()
public move(n: bigint, sig: Sig) {
// check position `n`
assert(n >= 0n && n < 9n);
// check signature `sig`
let player: PubKey = this.isAliceTurn ? this.alice : this.bob;
assert(this.checkSig(sig, player), `checkSig failed, pubkey: ${player}`);
// update stateful properties to make the move
assert(this.board[Number(n)] === TicTacToe.EMPTY, `board at position ${n} is not empty: ${this.board[Number(n)]}`);
let play = this.isAliceTurn ? TicTacToe.ALICE : TicTacToe.BOB;
this.board[Number(n)] = play;
this.isAliceTurn = !this.isAliceTurn;
// build the transation outputs
let outputs = toByteString('');
if (this.won(play)) {
outputs = Utils.buildPublicKeyHashOutput(hash160(player), this.ctx.utxo.value);
}
else if (this.full()) {
const halfAmount = this.ctx.utxo.value / 2n;
const aliceOutput = Utils.buildPublicKeyHashOutput(hash160(this.alice), halfAmount);
const bobOutput = Utils.buildPublicKeyHashOutput(hash160(this.bob), halfAmount);
outputs = aliceOutput + bobOutput;
}
else {
// build a output that contains latest contract state.
outputs = this.buildStateOutput(this.ctx.utxo.value);
}
if (this.changeAmount > 0n) {
outputs += this.buildChangeOutput();
}
// make sure the transaction contains the expected outputs built above
assert(this.ctx.hashOutputs === hash256(outputs), "check hashOutputs failed");
}
@method()
won(play: bigint): boolean {
let lines: FixedArray<FixedArray<bigint, 3>, 8> = [
[0n, 1n, 2n],
[3n, 4n, 5n],
[6n, 7n, 8n],
[0n, 3n, 6n],
[1n, 4n, 7n],
[2n, 5n, 8n],
[0n, 4n, 8n],
[2n, 4n, 6n]
];
let anyLine = false;
for (let i = 0; i < 8; i++) {
let line = true;
for (let j = 0; j < 3; j++) {
line = line && this.board[Number(lines[i][j])] === play;
}
anyLine = anyLine || line;
}
return anyLine;
}
@method()
full(): boolean {
let full = true;
for (let i = 0; i < 9; i++) {
full = full && this.board[i] !== TicTacToe.EMPTY;
}
return full;
}
}
Contract Properties
The tic-tac-toe contract supports two players and their public keys need to be saved. It contains the following contract properties:
Two stateless properties
alice
andbob
, both of which are PubKey type.Two stateful properties:
is_alice_turn
: a boolean. It represents whether it is alice's turn to play.board
: a fixed-size array FixedArray with a size of 9. It represents the state of every square in the board.Three constants:
EMPTY
, type bigint, value 0n. It means that a square in the board is empty-
ALICE
, type bigint, value 1n. Alice places symbol X in a square.
BOB
, type bigint, value 2n. Bob places symbol O in a square.Constructor
all the non-static properties are initialized in the constructor. at first, the entire board is empty.
constructor(alice: PubKey, bob: PubKey) {
super(...arguments);
this.alice = alice;
this.bob = bob;
this.is_alice_turn = true;
this.board = fill(TicTacToe.EMPTY, 9);
}
Public Methods
The TicTacToe contract contains a public@method
named move()
, which accepts two parameters. Alice
and Bob
engage in the game by sequentially invoking move()
after each locking X bitcoins in a UTXO associated with the TicTacToe
contract.
@method()
public move(n: bigint, sig: Sig) {
assert(n >= 0n && n < 9n);
}
After deploying the game contract, it becomes publicly accessible for viewing and potential interaction. To ensure that only the intended player can update the contract when it's their turn, an authentication mechanism is required. This is accomplished through the use of digital signatures.
this.checkSig()
is used to verify a signature against a public key. Use it to verify the sig parameter against the desired player in move()
, identified by their public key stored in the contract's properties.
// check signature `sig`
let player: PubKey = this.is_alice_turn ? this.alice : this.bob;
assert(this.checkSig(sig, player), `checkSig failed, pubkey: ${player}`);
Non-Public Methods
The TicTacToe
contract have two Non-Public methods:
won()
: iterate over thelines
array to check if a player has won the game. returnsboolean
type.full()
: traverse all the squares of the board to check if all squares of the board have symbols. returnsboolean
type.
Customise a contract transaction builder
we need to customise a transaction builder for our public method move()
static buildTxForMove(
current: TicTacToe,
options: MethodCallOptions<TicTacToe>,
n: bigint
): Promise<ContractTransaction> {
const play = current.isAliceTurn ? TicTacToe.ALICE : TicTacToe.BOB;
const nextInstance = current.next();
nextInstance.board[Number(n)] = play;
nextInstance.isAliceTurn = !current.isAliceTurn;
const unsignedTx: bsv.Transaction = new bsv.Transaction().addInput(
current.buildContractInput(options.fromUTXO)
);
if (nextInstance.won(play)) {
const script = Utils.buildPublicKeyHashScript(
hash160(current.isAliceTurn ? current.alice : current.bob)
);
unsignedTx.addOutput(
new bsv.Transaction.Output({
script: bsv.Script.fromHex(script),
satoshis: current.balance,
})
);
if (options.changeAddress) {
unsignedTx.change(options.changeAddress);
}
return Promise.resolve({
tx: unsignedTx,
atInputIndex: 0,
nexts: [],
});
}
if (nextInstance.full()) {
const halfAmount = current.balance / 2;
unsignedTx
.addOutput(
new bsv.Transaction.Output({
script: bsv.Script.fromHex(
Utils.buildPublicKeyHashScript(hash160(current.alice))
),
satoshis: halfAmount,
})
)
.addOutput(
new bsv.Transaction.Output({
script: bsv.Script.fromHex(
Utils.buildPublicKeyHashScript(hash160(current.bob))
),
satoshis: halfAmount,
})
);
if (options.changeAddress) {
unsignedTx.change(options.changeAddress);
}
return Promise.resolve({
tx: unsignedTx,
atInputIndex: 0,
nexts: [],
});
}
unsignedTx.setOutput(0, () => {
return new bsv.Transaction.Output({
script: nextInstance.lockingScript,
satoshis: current.balance,
});
});
if (options.changeAddress) {
unsignedTx.change(options.changeAddress);
}
const nexts = [
{
instance: nextInstance,
atOutputIndex: 0,
balance: current.balance,
},
];
return Promise.resolve({
tx: unsignedTx,
atInputIndex: 0,
nexts,
next: nexts[0],
});
}
Integrate front-end
after writing and testing our contract, the front-end gives user t an interface to interact and play the the Tic Tac Toe game. However, we need to compile our contract using this line of code:
npx scrypt-cli@latest compile
this should generate tictactoe.json
artifact file in the artifact directory
which can be further use to initialize the contract at the frontend.
import { TicTacToe } from './contracts/tictactoe';
import artifact from '../artifacts/tictactoe.json';
TicTacToe.loadArtifact(artifact);
Connect to wallet
before deploying the contract, you need to connect to a wallet. but first thing first,
install yours wallet
After installing the wallet, click the
settings
button in the upper right corner to switch totestnet
.
Then copy your wallet address and follow the guide here to get funded.
so, after that been done, you can request access to the wallet, you can use its requestAuth
method and also call getDefaultPubKey()
to get its public key.
const walletLogin = async () => {
try {
const provider = new DefaultProvider({
network: bsv.Networks.testnet
});
const signer = new PandaSigner(provider);
signerRef.current = signer;
const { isAuthenticated, error } = await signer.requestAuth()
if (!isAuthenticated) {
throw new Error(error)
}
setConnected(true);
const alicPubkey = await signer.getDefaultPubKey();
setAlicePubkey(toHex(alicPubkey))
// Prompt user to switch accounts
} catch (error) {
console.error("pandaLogin failed", error);
alert("pandaLogin failed")
}
};
Initialize the contract
the contract is initialized with the public keys of two players alice
and bob
. The public key can be obtained through calling getDefaultPubKey()
of Signer.
The following code initializes the contract.
const [alicePubkey, setAlicePubkey] = useState("");
const [bobPubkey, setBobPubkey] = useState("");
...
const startGame = async (amount: number) => {
try {
const signer = signerRef.current as PandaSigner;
const instance = new TicTacToe(
PubKey(toHex(alicePubkey)),
PubKey(toHex(bobPubkey))
);
await instance.connect(signer);
} catch(e) {
console.error('deploy TicTacToe failes', e)
alert('deploy TicTacToe failes')
}
};
Call the contract
const { tx: callTx } = await p2pkh.methods.unlock(
(sigResponses: SignatureResponse[]) => findSig(sigResponses, $publickey),
$publickey,
{
pubKeyOrAddrToSign: $publickey.toAddress()
} as MethodCallOptions<P2PKH>
);
Every move is a call to the contract and causes a change in the state of the contract.
Once you've completed working on the front-end, you can simply execute it by running
npm start
You can then access it through your browser at http://localhost:3000/.
Conclusion
Through this comprehensive guide, we've navigated the intricate landscape of BSV blockchain dAPP development using sCrypt, from smart contract creation to front-end integration, unlocking the potential for endless innovation using sCrypt on BSV blockchain. learn more about sCrypt from its comprehensive documentation here
Top comments (0)