DEV Community

John Godwin
John Godwin

Posted on

The Complete Guide To FullStack BSV Development.

Image description

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.

description
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;

  1. Install Node.js (require version >=16) and NPM on your machine by following the instructions over here

  2. Install Git

  3. 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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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 and bob, 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);
}
Enter fullscreen mode Exit fullscreen mode

Public Methods

The TicTacToe contract contains a public@methodnamed 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 TicTacToecontract.

@method()
public move(n: bigint, sig: Sig) {
    assert(n >= 0n && n < 9n);
}
Enter fullscreen mode Exit fullscreen mode


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}`);
Enter fullscreen mode Exit fullscreen mode

Non-Public Methods

The TicTacToe contract have two Non-Public methods:

  • won() : iterate over the lines array to check if a player has won the game. returns boolean type.

  • full() : traverse all the squares of the board to check if all squares of the board have symbols. returns boolean 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],
    });
  }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 to testnet.

description1
description2
Then copy your wallet address and follow the guide here to get funded.

description3
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")
    }
};
Enter fullscreen mode Exit fullscreen mode

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')
  }
};
Enter fullscreen mode Exit fullscreen mode

Call the contract

const { tx: callTx } = await p2pkh.methods.unlock(
    (sigResponses: SignatureResponse[]) => findSig(sigResponses, $publickey),
    $publickey,
    {
        pubKeyOrAddrToSign: $publickey.toAddress()
    } as MethodCallOptions<P2PKH>
);
Enter fullscreen mode Exit fullscreen mode

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)