DEV Community

sCrypt
sCrypt

Posted on

The Complete Guide to Full Stack Bitcoin SV Development

Image description

In this guide, you’ll learn a web3 tech stack that will allow you to build full stack decentralized apps on the Bitcoin SV blockchain. We will walk through the entire process of building a full stack decentralized Tic-Tac-Toe, including:

  • Write a smart contract.
  • Deploy the contract
  • Add a front-end (React)
  • Integrate Yours wallet

By the end, you will have a fully functional Tic-Tac-Toe App running on Bitcoin.

Image description

What we will use

Let’s go over the main pieces we will be using and how they fit into the stack.

1 - sCrypt Framework

sCrypt is a TypeScript framework to develop smart contracts on Bitcoin. It offers a complete tech stack:

  • the sCrypt language: an embedded
    Domain Specific Language
    (eDSL) based on TypeScript, which allows developers to write
    smart contracts directly in TypeScript. Developers don’t have
    to learn a new Web3 programming language like Solidity, and can
    reuse their favorite tools, like IDEs and NPM.

  • library (scrypt-ts):
    a comprehensive and concise library
    designed for client-side JavaScript applications, such as
    React, Vue, Angular, or Svelte, to interact with the Bitcoin SV
    Blockchain and its ecosystem.

  • sCrypt CLI: CLI to
    easily create, compile and publish sCrypt
    projects. The CLI provides best practice project scaffolding.

2. Yours Wallet

Yours Wallet is an open-source digital wallet for BSV and 1Sat Ordinals that enables access to decentralized applications developed on Bitcoin SV. Yours Wallet generates and manages private keys for its users in a non-custodial manner, ensuring that the users have full control over their funds and transactions. These keys can be utilized within the wallet to securely store funds and authorize transactions.

3. React

React.js, often simply referred to as React, is a JavaScript library developed by Facebook. It’s primarily used for building user interfaces (UIs) for web applications. It simplifies the process of building dynamic and interactive web applications and continues to be seemingly dominating the front-end space.

What we will Build

We will build a very simple Tic-Tac-Toe game on chain. It uses the Bitcoin addresses of two players (Alice and Bob respectively) to initialize a smart contract. 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. Tic-Tac-Toe, the age-old game of strategy and skill, has now found its way onto the blockchain thanks to the power of sCrypt.

Prerequisites

1 - Install node.js and npm (node.js ≥ version 16)
2 - Install Git
3 - Yours wallet Chrome extension installed in your browser
4 - Install sCrypt CLI
npm install -g scrypt-cli

Getting Started

Let’s simply create a new React project.

Firstly let’s create a new React project with TypeScript template.

npx create-react-app tic-tac-toe --template typescript

Then, change the directory to the tic-tac-toe project directory and also run the init command of the CLI to add sCrypt support in your project.

cd tic-tac-toe
npx scrypt-cli@latest init

Tic-tac-toe Contract
Next, let’s 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

Properties
The Tic-Tac-Toe contract features several essential properties that define its functionality:

1 - Alice and Bob: Public keys of the two players.
2 - is_alice_turn: A boolean flag indicating whose turn it is
to play.
3 - board: A representation of the game board, stored as a
fixed-size array.
4 - Constants: Three static properties defining game symbols
and empty squares.

Constructor

constructor(alice: PubKey, bob: PubKey) {
        super(...arguments)
        this.alice = alice;
        this.bob = bob;
        this.isAliceTurn = true;
        this.board = fill(TicTacToe.EMPTY, 9);
    }
Enter fullscreen mode Exit fullscreen mode

Upon deployment, the constructor initializes the contract with the public keys of Alice and Bob. Additionally, it sets up an empty game board to kickstart the game play.

Public methods
Each contract must have at least one public @method. It is denoted with the public modifier and does not return any value. It is visible outside the contract and acts as the main method into the contract (like main in C and Java).

The public method in the contract is move(), which allows players to make their moves on the board. This method validates the moves, checks the player's signature, updates the game state, and determines the outcome of the game.

Signature verification

Once the game contract is deployed, anyone can view and potentially interact with it. We need an authentication mechanism to ensure only the desired player can update the contract if it’s their turn. This is achieved using ditigal signatures.

Only the authorized player can make a move during their turn, validated through their respective public key stored in the contract.

// check signature `sig`
let player: PubKey = this.isAliceTurn ? this.alice : this.bob;
assert(this.checkSig(sig, player), `checkSig failed, pubkey: ${player}`);
Enter fullscreen mode Exit fullscreen mode

Non-Public methods

The contract includes two non-public methods, won() and full(), responsible for determining whether a player has won the game and if the board is full, leading to a draw.

Tx Builder: buildTxForMove

Bitcoin transaction can have multiple inputs and outputs. We need to build a transaction when calling a contract.

Here, we have implement a customize transaction builder for the move() method as below:

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 (React)

After we have written/tested our contract, we can integrate it with front-end so that users can play our game.

First, let’s compile the contract and get the contract artifact json file by running the command below :

npx scrypt-cli@latest compile

Image description

You should see an artifact file tictactoe.json in the artifacts directory. It can be used to initialize a contract at the front end.

import { TicTacToe } from './contracts/tictactoe';
import artifact from '../artifacts/tictactoe.json';

TicTacToe.loadArtifact(artifact);
Enter fullscreen mode Exit fullscreen mode

Install and Fund Wallet

Before deploying a contract, we need to connect a wallet first. We use Yours Wallet, a MetaMask-like wallet.

After installing the wallet, click the settings button in the upper right corner to switch to testnet. Then copy your wallet address and go to our faucet to fund it.

Image description

Connect to wallet

We call requestAuth() to request to connect to the wallet. If the request is approved by the user, we now have full access to the wallet. We can, for example, 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

We have obtained the contract class Tictactoe by loading the contract artifact file. When a user clicks the start button, 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

Now we can start playing the game. Every move is a call to the contract and triggers a change in the state of 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

After finishing with the front-end you can simply run :
npm start

You can now view it at http://localhost:3000/ in your browser.

Image description

Conclusion

Congratulations! You have just built your first full stack dApp on Bitcoin. Now you can play tic-tac-toe or build your favorite game on Bitcoin. Now would be a good time to pop some champagne if you haven’t already :).

One session of play can be viewed here:

https://youtu.be/l65Ha5AmKjk

All the code can be found at this github repo.

By default, we deploy the contract on testnet. You can easily change it to mainnet.

Top comments (0)