Esta publicación se publicó por primera vez en Medium.

En esta guía, aprenderá una pila de tecnología Web3 que le permitirá crear aplicaciones descentralizadas de pila completa en la cadena de bloques de Bitcoin SV. Recorreremos todo el proceso de construcción de un Tic-Tac-Toe descentralizado de pila completa, que incluye:

  • Escribe un contrato inteligente.
  • Implementar el contrato
  • Agregar una interfaz (React)
  • Integra tu billetera

Al final, tendrás una aplicación Tic-Tac-Toe completamente funcional ejecutándose en Bitcoin.

Imagen de presentación de Tic-Tac-Toe

que usaremos

Repasemos las piezas principales que usaremos y cómo encajan en la pila.

1. Marco de cifrado

sCrypt es un marco TypeScript para desarrollar contratos inteligentes en Bitcoin. Ofrece una pila tecnológica completa:

  • El lenguaje sCrypt: un lenguaje específico de dominio (eDSL) integrado basado en TypeScript, que permite a los desarrolladores escribir contratos inteligentes directamente en TypeScript. Los desarrolladores no tienen que aprender un nuevo lenguaje de programación Web3 como Solidity y pueden reutilizar sus herramientas favoritas, como IDE y NPM.
  • biblioteca (scrypt-ts): una biblioteca completa y concisa diseñada para aplicaciones JavaScript del lado del cliente, como React, Vue, Angular o Svelte, para interactuar con Bitcoin SV Blockchain y su ecosistema.
  • sCrypt CLI: CLI para crear, compilar y publicar fácilmente proyectos sCrypt. La CLI proporciona un andamiaje de proyectos de mejores prácticas.

2. Tu billetera

Tu billetera es una billetera digital de código abierto para BSV y 1Sat Ordinals que permite el acceso a aplicaciones descentralizadas desarrolladas en Bitcoin SV. Yours Wallet genera y administra claves privadas para sus usuarios sin custodia, asegurando que los usuarios tengan control total sobre sus fondos y transacciones. Estas claves se pueden utilizar dentro de la billetera para almacenar fondos de forma segura y autorizar transacciones.

3. Reaccionar

React.js, a menudo denominado simplemente React, es una biblioteca de JavaScript desarrollada por Facebook. Se utiliza principalmente para crear interfaces de usuario (UI) para aplicaciones web. Simplifica el proceso de creación de aplicaciones web dinámicas e interactivas y aparentemente sigue dominando el espacio front-end.

Lo que construiremos

Construiremos un juego de tres en raya muy sencillo en cadena. Utiliza las direcciones de Bitcoin de dos jugadores (Alice y Bob respectivamente) para inicializar un contrato inteligente. Cada uno apuesta la misma cantidad y la fija en el contrato. El ganador se lleva todos los bitcoins bloqueados en el contrato. Si nadie gana y hay empate, los dos jugadores pueden retirar cada uno la mitad del dinero. Tic-Tac-Toe, el antiguo juego de estrategia y habilidad, ahora ha llegado a blockchain gracias al poder de sCrypt.

Requisitos previos

  1. Instale node.js y npm (node.js ≥ versión 16)
  2. Instalar Git
  3. Extensión de Chrome Yours Wallet instalada en su navegador
  4. Instalar la CLI de sCrypt
npm install -g scrypt-cli

Empezando

Simplemente creemos un nuevo proyecto de React.

Primero, creemos un nuevo proyecto de React con una plantilla de TypeScript.

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

Luego, cambie el directorio al directorio del proyecto tic-tac-toe y también ejecute el comando init de la CLI para agregar compatibilidad con sCrypt en su proyecto.

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

Contrato tres en raya

A continuación, creemos un contrato en src/contratos/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;
    }

}

Propiedades

El contrato Tic-Tac-Toe presenta varias propiedades esenciales que definen su funcionalidad:

  1. Alicia y Bob: Claves públicas de los dos jugadores.
  2. es_alice_turn: Una bandera booleana que indica a quién le toca jugar.
  3. junta: una representación del tablero de juego, almacenada como una matriz de tamaño fijo.
  4. Constantes: Tres propiedades estáticas que definen símbolos del juego y casillas vacías.

Constructor

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

Tras la implementación, el constructor inicializa el contrato con las claves públicas de Alice y Bob. Además, configura un tablero de juego vacío para iniciar el juego.

Métodos públicos

Cada contrato debe tener al menos un @method público. Se denota con el modificador público y no devuelve ningún valor. Es visible fuera del contrato y actúa como método principal dentro del contrato (como main en C y Java).

El método público en el contrato es move(), que permite a los jugadores realizar sus movimientos en el tablero. Este método valida los movimientos, verifica la firma del jugador, actualiza el estado del juego y determina el resultado del juego.

Verificación de firma

Una vez que se implementa el contrato del juego, cualquiera puede verlo y potencialmente interactuar con él. Necesitamos un mecanismo de autenticación para garantizar que solo el jugador deseado pueda actualizar el contrato si es su turno. Esto se logra mediante firmas digitales.

Sólo el jugador autorizado puede realizar un movimiento durante su turno, validado a través de su respectiva clave pública almacenada en el contrato.

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

Métodos no públicos

El contrato incluye dos métodos no públicos, won() y full(), responsables de determinar si un jugador ha ganado el juego y si el tablero está lleno, lo que lleva a un empate.

Generador de Tx: buildTxForMove

Las transacciones de Bitcoin pueden tener múltiples entradas y salidas. Necesitamos crear una transacción al llamar a un contrato.

Aquí, hemos implementado un generador de transacciones personalizado para el método move() como se muestra a continuación:

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],
    });
  }

Integrar front-end (React)

Después de haber escrito/probado nuestro contrato, podemos integrarlo con el front-end para que los usuarios puedan jugar nuestro juego.

Primero, compilemos el contrato y obtengamos el archivo json del artefacto del contrato ejecutando el siguiente comando:

npx scrypt-cli@latest compile

Captura de pantalla del códigoCaptura de pantalla del código

Debería ver un archivo de artefacto tictactoe.json en el directorio de artefactos. Se puede utilizar para inicializar un contrato en el front-end.

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

TicTacToe.loadArtifact(artifact);

Instalar y financiar billetera

Antes de implementar un contrato, primero debemos conectar una billetera. Usamos Yours Wallet, una billetera similar a MetaMask.

Después de instalar la billetera, haga clic en el botón de configuración en la esquina superior derecha para cambiar a testnet. Luego copie la dirección de su billetera y vaya a nuestro grifo para financiarla.

Captura de pantalla del sitio web sCryptCaptura de pantalla del sitio web sCrypt

Conectarse a la billetera

Llamamos a requestAuth() para solicitar conectarse a la billetera. Si el usuario aprueba la solicitud, ahora tenemos acceso completo a la billetera. Podemos, por ejemplo, llamar a getDefaultPubKey() para obtener su clave pública.

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")
    }
};

Inicializar el contrato

Hemos obtenido la clase de contrato. tictacto cargando el archivo de artefacto del contrato. Cuando un usuario hace clic en el comenzar Botón, el contrato se inicializa con las claves públicas de dos jugadores, Alice y Bob. La clave pública se puede obtener llamando etDefaultPubKey() del firmante.

El siguiente código inicializa el contrato.

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')
  }
};

llamar al contrato

Ahora podemos empezar a jugar. Cada movimiento es una llamada al contrato y desencadena un cambio en el estado del contrato.

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

Después de terminar con el front-end, simplemente puedes ejecutar:

npm start

Ahora puedes verlo en `http://localhost:3000/` en tu navegador.

Tres en raya GIFTres en raya GIF

Conclusión

¡Felicidades! Acaba de crear su primera dApp de pila completa en Bitcoin. Ahora puedes jugar al tres en raya o crear tu juego favorito con Bitcoin. Ahora sería un buen momento para tomar un poco de champán si aún no lo has hecho :).

Una sesión de juego se puede ver aquí:

Video de YoutubeVideo de Youtube

Todo el código se puede encontrar en este repositorio de github.

De forma predeterminada, implementamos el contrato en testnet. Puedes cambiarlo fácilmente a mainnet.

Ver: sCrypt Hackathon 2024 (17 de marzo de 2024, p. m.)

Video de YoutubeVideo de Youtube

¿Nuevo en blockchain? Consulte la sección Blockchain para principiantes de CoinGeek, la guía de recursos definitiva para aprender más sobre la tecnología blockchain.

Share.
Leave A Reply