Cada tutorial ha sido probado con las últimas versiones de:
Para instalar zkApp CLI:
$ npm install -g zkapp-cli
Para confirmar la instalación exitosa:
$ zk --version
Para utilizar zkApp CLI y o1js, tu entorno requiere:
--experimental-wasm-threads
)Usa un gestor de paquetes para instalar las versiones requeridas y actualizar versiones anteriores si es necesario. Los gestores de paquetes para los entornos compatibles son:
En Linux, podrías necesitar instalar una versión reciente de Node.js utilizando NodeSource. Usa deb
o rpm
tal y como es recomendado por el projecto Node.js.
Para verificar tus versiones instaladas, usa npm -v
, node -v
y git -v
.
Este tutorial de Hola Mundo te ayuda a comenzar con o1js, zkApps y la programación con pruebas de conocimiento cero.
En este tutorial paso a paso, aprenderás a codificar una zkApp de principio a fin.
Harás lo siguiente:
Los tutoriales posteriores introducen más conceptos y patrones.
El código fuente completo para este tutorial se proporciona en el directorio examples/zkapps/01-hello-world en GitHub. Mientras estés allí, dale una estrella al repositorio /docs2
para que otros desarrolladores zk puedan aprender a construir una zkApp!
Asegúrate de que tu entorno cumpla con los prerrequisitos que se mencionan al inicio de este documento.
En particular, asegúrate de tener instalado el zkApp CLI:
Ahora que tienes las herramientas instaladas, puedes comenzar a construir tu aplicación.
zk project
:El comando zk project tiene la capacidad de crear la interfaz de usuario para tu proyecto. Para este tutorial, selecciona none:
La salida esperada es:
El comando zk project
crea el directorio 01-hello-world
que contiene la estructura para tu proyecto, incluyendo herramientas como la herramienta de formateo de código Prettier, la herramienta de análisis de código estático ESLint y el framework de pruebas de JavaScript Jest.
La salida muestra estos resultados:
Para este tutorial, ejecutarás comandos desde la raíz del directorio 01-hello-world
mientras trabajas en el directorio src
en archivos que contienen el código TypeScript para el contrato inteligente. Cada vez que realices actualizaciones, y luego compiles o despliegues, el código TypeScript se compila en JavaScript en el directorio build
.
Comienza eliminando los archivos predeterminados que vienen con el nuevo proyecto.
$ rm src/Add.ts
$ rm src/Add.test.ts
$ rm src/interact.ts
$ zk file src/Square
$ touch src/main.ts
zk file
creó los archivos src/Square.ts
y src/Square.test.ts
.main.ts
como un script para interactuar con el contrato inteligente y observar cómo funciona.En tutoriales posteriores, aprenderás a interactuar con un contrato inteligente desde el navegador, como lo haría un usuario típico.
src/index.ts
en un editor de texto y cámbialo para que se vea así:
1 import { Square } from './Square.js';
2
3 export { Square };
El archivo src/index.ts
contiene todas las exportaciones que deseas hacer disponibles para su consumo desde fuera de tu proyecto de contrato inteligente, como desde una UI.
¡Ahora, la parte divertida! Escribe tu contrato inteligente en el archivo src/Square.ts
.
Los números de línea se proporcionan para mayor comodidad. Una versión final del contrato inteligente se proporciona en el archivo de ejemplo Square.ts.
Esta parte del tutorial te guía a través del código del contrato inteligente Square
ya completado en el archivo de ejemplo src/Square.ts
.
Copia el siguiente código en el archivo src/Square.ts
:
Ahora estás listo para revisar las importaciones en el contrato inteligente.
La declaración import
trae otros paquetes y dependencias para usar en tu contrato inteligente.
Todas las funciones utilizadas dentro de un contrato inteligente deben operar con tipos de datos compatibles con o1js: tipos Field
y otros tipos construidos sobre tipos Field
.
Estos elementos son:
Field
: El tipo de número nativo en o1js. Puedes pensar en los elementos Field como enteros sin signo. Los elementos Field son el tipo más básico en o1js. Todos los demás tipos compatibles con o1js se construyen sobre elementos Field.SmartContract
: La clase que crea contratos inteligentes zkApp.state
: Un decorador de conveniencia utilizado en contratos inteligentes zkApp para crear referencias al estado almacenado on-chain en una cuenta zkApp.State
: Una clase utilizada en contratos inteligentes zkApp para crear un estado almacenado on-chain en una cuenta zkApp.method
: Un decorador de conveniencia utilizado en contratos inteligentes zkApp para crear métodos de contrato inteligente como funciones. Los métodos que utilizan este decorador son los puntos de entrada del usuario final para interactuar con un contrato inteligente.Ahora, revisa el contrato inteligente en el archivo src/Square.ts
.
El contrato inteligente llamado Square
tiene un elemento de estado on-chain llamado num
de tipo Field
como se define en el siguiente código:
Los zkApps pueden tener hasta ocho campos de estado on-chain. Cada campo almacena hasta 32 bytes (técnicamente, 31.875 bytes o 255 bits) de datos arbitrarios. Un tutorial posterior cubre opciones para el estado off-chain.
Ahora, este código agrega el método init
para configurar el estado inicial del contrato inteligente al implementarlo:
Dado que estás extendiendo SmartContract
, que tiene su propia inicialización, llamar a super.init()
invoca esta función en la clase base.
Luego, this.num.set(Field(3))
inicializa el estado on-chain num
a un valor de 3
.
Opcionalmente, puedes especificar permisos. Consulta setPermissions en la documentación de referencia de o1js.
Finalmente, este código agrega la función update()
:
El nombre de la función update
es arbitrario, pero tiene sentido para este ejemplo. Observa cómo se usa el decorador @method
porque está destinado a ser invocado por usuarios finales utilizando una interfaz de usuario zkApp o, como en este caso, el script main.ts
.
Este método contiene la lógica por la cual los usuarios finales pueden actualizar el estado on-chain de la cuenta zkApp.
Una cuenta zkApp es una cuenta en la blockchain de Mina donde se implementa un contrato inteligente zkApp. Una cuenta zkApp tiene asociada una clave de verificación.
En este ejemplo, el código especifica:
update()
que es el cuadrado del estado on-chain existente denominado num
(por ejemplo, 3), entonces actualiza el valor de num
almacenado on-chain al valor proporcionado (en este caso, 9).Estas condiciones de actualización se logran utilizando afirmaciones dentro del método. Cuando un usuario invoca un método en un contrato inteligente, todas las afirmaciones deben ser verdaderas para generar la prueba de conocimiento cero de ese contrato inteligente. La red Mina acepta la transacción y actualiza el estado on-chain solo si la prueba adjunta es válida. Esta afirmación es cómo puedes lograr un comportamiento predecible en un modelo de ejecución off-chain.
Observa que se utilizan los métodos get()
y set()
para recuperar y establecer el estado on-chain.
get()
dentro de él.set()
cambia la transacción para indicar que los cambios en este estado on-chain particular se actualizan solo cuando la transacción es recibida por la red Mina si contiene una autorización válida (generalmente, una autorización válida es una prueba).La lógica también utiliza el método .mul()
para la multiplicación de los valores almacenados en tipos Field
. Puedes ver todos los métodos disponibles en la documentación de referencia de o1js.
Recuerda que las funciones en tu contrato inteligente deben operar con tipos de datos compatibles con o1js: tipos Field
y otros tipos construidos sobre tipos Field
. Debido a que un contrato inteligente es realmente un circuito de conocimiento cero, las funciones de paquetes NPM arbitrarios funcionan dentro de un contrato inteligente solo si las funciones que proporciona el contrato operan con tipos de datos compatibles con o1js.
Importante, los datos pasados como entrada a un método de contrato inteligente en o1js son privados y nunca son vistos por la red.
También puedes almacenar datos públicamente on chain cuando sea necesario, como num
en este ejemplo. Un tutorial posterior cubre un ejemplo que implementa la privacidad.
Felicidades, has revisado el código completo del contrato inteligente.
A continuación, escribirás un script que interactúa con tu contrato inteligente. Para tu referencia, el archivo de ejemplo completo se proporciona en main.ts. Sigue estos pasos para construir el archivo main.ts
para que puedas interactuar con el contrato inteligente.
Para este tutorial, la declaración import
trae elementos de o1js
que utilizas para interactuar con tu contrato inteligente.
src/main.ts
:Estos elementos importados son:
Field
: El mismo tipo de entero sin signo de o1js que aprendiste anteriormente.Mina
: Una blockchain local de Mina para desplegar el contrato inteligente para que puedas interactuar con él como lo haría un usuario.PrivateKey
: Una clase con funciones para manipular claves privadas.AccountUpdate
: Una clase que genera una estructura de datos que puede actualizar cuentas zkApp.Usar una blockchain local acelera el desarrollo y prueba el comportamiento de tu contrato inteligente localmente. Los tutoriales posteriores cubren cómo desplegar un zkApp en redes Mina en vivo.
Para inicializar tu blockchain local, agrega el siguiente código a src/main.ts
:
Esta blockchain local también proporciona cuentas prefinanciadas. Estas líneas crean cuentas de prueba locales con MINA de prueba para usar en este tutorial:
Ahora que el contrato inteligente Square está completo, estos comandos ejecutan tu proyecto como una blockchain local.
Para compilar el código TypeScript en JavaScript:
Para ejecutar el código JavaScript:
Tienes la opción de combinar estos comandos en una sola línea:
npm run build
crea código JavaScript en el directorio build
.&&
enlaza dos comandos juntos. El segundo comando se ejecuta solo si el primer comando es exitoso.node build/src/main.js
ejecuta el código en src/main.ts
.Para inicializar tu contrato inteligente, vamos a agregar más código al archivo src/main.ts
.
Todos los contratos inteligentes que creas con la CLI de zkApp utilizan un código similar:
Square
y desplegarlo en zkAppAddress
Square
después del despliegueLos comentarios desglosan cada etapa:
Intenta ejecutar este comando de nuevo:
La salida esperada es:
Para actualizar tu cuenta zkApp local con una transacción, agrega el siguiente código al archivo src/main.ts
:
Este código crea una nueva transacción que intenta actualizar el campo al valor 9
. Debido a las reglas en la función update()
que se llama en el contrato inteligente, este comando tiene éxito cuando lo ejecutas nuevamente:
Es hora de hacer algunas pruebas. La lógica del contrato permite que el número almacenado como estado on-chain sea reemplazado solo por su cuadrado. Ahora que num
está en estado 9
, la actualización es posible solo con 81
.
Para probar un fallo, actualiza el estado a 75 en zkAppInstance.update(Field(75))
:
Intenta ejecutar este comando de nuevo:
La salida esperada es:
Y finalmente, asegúrate de cambiar tu archivo main.ts
para incluir la actualización correcta:
Ejecuta este comando de nuevo:
La salida esperada es:
Puedes seguir este video donde el especialista en criptpgrafía, David Wong, aprende a codificar un proyecto Hola Mundo: Aprende a programar zkApps conmigo
El video se proporciona con fines educativos y utiliza versiones anteriores de zkApp CLI y o1js, por lo que hay algunas diferencias.
¡Felicidades! Has completado con éxito todos los pasos para construir tu primera zkApp con o1js.
Consulta el Tutorial 2: Entradas Privadas y Funciones Hash para aprender a usar entradas privadas y funciones hash con o1js.
Encuentra más tutoriales y recursos en la documentación de zkApps.
En el tutorial Hola Mundo, construiste un contrato inteligente zkApp básico con o1js con una única variable de estado que podía actualizarse si conocías el cuadrado de ese número.
En este tutorial, aprenderás sobre entradas privadas y funciones hash.
Con un zkApp, el dispositivo local del usuario del contrato inteligente genera una o más pruebas de conocimiento cero, que luego son verificadas por la red Mina. Cada método en un contrato inteligente o1js corresponde a la construcción de una prueba.
Todas las entradas a un contrato inteligente son privadas por defecto. Las entradas nunca son vistas por la blockchain a menos que almacenes esos valores como estado on-chain en la cuenta zkApp.
En este tutorial, construirás un contrato inteligente con una pieza de estado privado que puede ser modificada si un usuario conoce el estado privado.
Este tutorial ha sido probado con Mina zkApp CLI versión 0.9.0
y o1js 0.11.0
.
Asegúrate de que tu entorno cumpla con los prerrequisitos mencionados al inicio de este documento.
Crea o cambia a un directorio donde tengas privilegios de escritura.
Crea un proyecto usando el comando zk project
:
El comando zk project
tiene la capacidad de crear la interfaz de usuario para tu proyecto. Para este tutorial, selecciona none
:
La salida esperada es:
El comando zk project
crea el directorio 02-private-inputs-and-hash-functions
que contiene la estructura para tu proyecto, incluyendo herramientas como la herramienta de formateo de código Prettier, la herramienta de análisis de código estático ESLint y el framework de pruebas de JavaScript Jest.
02-private-inputs-and-hash-functions
y lista los contenidos:La salida muestra estos resultados:
Para este tutorial, ejecutarás comandos desde la raíz del directorio 02-private-inputs-and-hash-functions
mientras trabajas en el directorio src
en archivos que contienen el código TypeScript para el contrato inteligente.
Cada vez que realices actualizaciones, para luego compilar o desplegar, el código TypeScript se compila en JavaScript en el directorio build
.
Comienza eliminando los archivos predeterminados que vienen con el nuevo proyecto.
zk file
creó el archivo src/IncrementSecret.ts
y el archivo de prueba src/IncrementSecret.test.ts
.main.ts
como un script para interactuar con el contrato inteligente y observar cómo funciona.src/index.ts
en un editor de texto y cámbialo para que se vea así:El archivo src/index.ts
contiene todas las exportaciones que deseas hacer disponibles para su consumo desde fuera de tu proyecto de contrato inteligente, como desde una interfaz de usuario.
Copia el siguiente código en tu archivo IncrementSecret.ts
:
Copia el siguiente código en tu archivo main.ts
:
Ahora construiremos el contrato inteligente para nuestra aplicación.
La declaración import
en el archivo IncrementSecret.ts
trae otros paquetes y dependencias para usar en tu contrato inteligente.
Todas las funciones utilizadas dentro de un contrato inteligente deben operar con tipos de datos compatibles con o1js: tipos Field
y otros tipos construidos sobre tipos Field
.
El contrato inteligente llamado IncrementSecret
tiene un elemento de estado on-chain llamado x
de tipo Field
como se define en el siguiente código:
Este código agrega la estructura básica para el contrato inteligente. Ya estás familiarizado con el código de importación y exportación del Tutorial Hola Mundo descrito arriba.
El método initState()
está destinado a ejecutarse una vez para configurar el estado inicial en la cuenta zkApp.
El método initState()
acepta tu secreto y agrega un valor salt
.
Estas entradas al método initState()
son privadas para quien inicializa el contrato. La cuenta zkApp en el blockchain no revela cuáles son los valores firstSecret
o salt
en realidad.
Este método actualiza el estado:
Mina utiliza la función de hash Poseidon que está optimizada para un rendimiento rápido dentro de los sistemas de prueba de conocimiento cero. La función de hash Poseidon toma un arreglo de Fields y devuelve un solo Field como salida.
Este contrato inteligente utiliza un número secreto y el segundo Field, salt
.
El método incrementSecret()
verifica que el hash del salt y el secreto sea igual al estado actual x
:
1
al secreto y establece x
como el hash del salt y este nuevo secreto.Debido a que los contratos inteligentes zkApp se ejecutan off-chain, tu salt y secreto permanecen privados y nunca se transmiten a ningún lugar.
Solo el resultado, actualizando el estado on-chain de x
a hash([ salt, secret + 1])
se revela. Debido a que el salt y el secreto no se pueden deducir de su hash, permanecen privados.
salt
El salt criptográfico añade una capa adicional de seguridad a un contrato inteligente. El argumento extra salt
previene un posible ataque al contrato inteligente. Si solo usas secret
, el contrato es vulnerable a ser descubierto por un atacante. Un atacante podría intentar hacer hash de secretos probables y luego verificar si el hash coincide con el hash almacenado en el contrato inteligente. Si el hash coincide, entonces el atacante sabe que ha descubierto el secreto. Este escenario es particularmente preocupante si el secreto es probable que esté dentro de un subconjunto particular de valores posibles, digamos entre 1 y 10,000. En ese caso, con solo 10,000 hashes, el atacante podría descubrir el secreto.
Agregar salt como una segunda entrada al código del contrato hace que sea más difícil para un atacante hacer ingeniería inversa y acceder al contrato. El salt hace que el contrato sea más seguro y ayuda a proteger los datos almacenados en él. Para una seguridad óptima, el salt es conocido solo por ti y suele ser aleatorio.
El archivo src/main.ts
es similar al tutorial Hola Mundo. Para una versión completa, consulta main.ts.
Para este tutorial, las partes clave a discutir son la inicialización de nuestro contrato y el uso del Poseidon hash.
La inicialización del contrato inteligente esta vez es:
Nota que el método initState()
acepta el salt y el secreto. En este caso, el secreto es el número 750
.
Este código crea una transacción de usuario y actualiza el estado on-chain:
Invoca la zkApp con el salt y el secreto (el número 750
).
Debido a que los zkApps son ejecutados localmente, ni el secreto ni la sal son parte de la transacción.
En su lugar, la transacción incluye solamente la prueba de que la actualización fue invocada de tal forma que todas las verificaciones pasaron y que se hizo una actualización al estado on-chain x
donde el valor del hash está almacenado. Luego de que la transacción es procesada por la red Mina, x
tendrá el valor Poseidon.hash([ salt, Field(750).add(1)])
. El salt y el secreto no son revelados.
Intenta correr main
:
La salida se ve algo como así:
Vamos ahora a intentar actualizar el valor del secreto con un número que no corresponde al valor on-chain. Agrega a src/main.ts
el siguiente código:
Deberías recibir un error similar a este:
Esto ocurre debido a que el número 1234
no corresponde con el valor secreto actualmente almacenado on-chain, que para este momento es 751
.
¡Felicidades! Has construido un contrato inteligente que utiliza privacidad y funciones hash.
A continuación aprenderás cómo desplegar zkApps en una red en vivo.
En tutoriales anteriores, aprendiste a desplegar y ejecutar transacciones en una red local.
En este tutorial, utilizarás el comando zk config
para crear un alias de despliegue, solicitar fondos de tMINA para pagar las tarifas de transacción y desplegar un proyecto en una red en vivo.
Las zkApps de Mina están disponibles solo en Berkeley, la testnet pública y con todas las características de Mina. Este tutorial reutiliza el contrato Square
que creaste en el Tutorial Hola Mundo.
Este tutorial ha sido probado con la versión 0.15.0
de zkApp CLI y la versión 0.14.1
de o1js.
Asegúrate de que tu entorno cumpla con los Prerrequisitos descritos arriba.
Si tienes versiones anteriores de zkApp CLI y o1js instaladas, asegúrate de actualizar tu zkApp CLI a la última versión:
zk project
:El comando zk project
tiene la capacidad de crear la interfaz de usuario para tu proyecto. Para este tutorial, selecciona none
:
El comando zk project
crea el directorio 03-deploying-to-a-live-network
que contiene la estructura para tu proyecto, incluyendo herramientas como la herramienta de formateo de código Prettier, la herramienta de análisis de código estático ESLint y el framework de pruebas de JavaScript Jest.
03-deploying-to-a-live-network
.Para este tutorial, ejecutarás comandos desde la raíz del directorio 03-deploying-to-a-live-network
mientras trabajas en el directorio src
en archivos que contienen el código TypeScript para el contrato inteligente.
Cada vez que realices actualizaciones, al compilar o desplegar, el código TypeScript se compila en JavaScript en el directorio build
.
El comando zk project
crea el directorio 03-deploying-to-a-live-network
que contiene la estructura para tu proyecto, incluyendo herramientas como la herramienta de formateo de código Prettier, la herramienta de análisis de código estático ESLint y el marco de pruebas de JavaScript Jest.
03-deploying-to-a-live-network
.Para este tutorial, ejecutarás comandos desde la raíz del directorio 03-deploying-to-a-live-network
mientras trabajas en el directorio src
en archivos que contienen el código TypeScript para el contrato inteligente.
Cada vez que realices actualizaciones, y luego compiles o despliegues, el código TypeScript se compila en JavaScript en el directorio build
.
Comienza eliminando los archivos predeterminados que vienen con el nuevo proyecto.
Elimina los archivos generados por defecto:
Ahora, crea un nuevo archivo para tu contrato inteligente:
Copia los archivos src/Square.ts
y src/index.ts
de los archivos de ejemplo del primer tutorial 01-hello-world/src a tu directorio local 03-deploying-to-a-live-network/src
. Si se solicita, reemplaza los archivos existentes.
Ahora que tu contrato inteligente está en su lugar, estás listo para desplegar tu contrato inteligente en la Testnet de Berkeley.
Ya instalaste Mina zkApp CLI como parte de los prerrequisitos, por lo que tienes las herramientas para gestionar despliegues.
En algunos casos, podrías necesitar crear una cuenta personalizada para tu zkApp, por ejemplo, para desplegar un zkApp con una clave diferente a la clave de pagador de tarifas, parametrizar programáticamente un zkApp antes de inicializarlo, o crear un contrato inteligente programáticamente para usuarios como parte de una aplicación. Para más detalles, consulta Interactuar con zkApps en el lado del servidor.
El archivo de configuración config.json
se creó automáticamente cuando creaste tu proyecto con el comando zk project
. Sin embargo, el archivo de configuración generado aún no contiene el alias de despliegue.
Los mensajes de guía del comando zk config
te ayudan a crear un alias de despliegue en tu archivo config.json
del proyecto.
Puedes tener uno o más alias de despliegue para tu proyecto.
Un alias de despliegue consiste en:
Un nombre auto-descriptivo (puede ser cualquier cosa). Utilizar patrones de nombres es útil cuando tienes más de un alias de despliegue.
La URL de la API GraphQL de Mina que define la red que recibe tu transacción de despliegue y la transmite a la red Mina apropiada (Testnet, Devnet, Mainnet, etc.)
La tarifa de transacción (en MINA) a usar al desplegar
Dos pares de claves:
keys/berkeley.json
.Alias de cuenta pagadora de tarifas
Para configurar tu despliegue, ejecuta el comando zk config
y responde a las indicaciones:
Para este tutorial en la Testnet de Berkeley, utiliza:
Nombre del alias de despliegue: berkeley
Este tutorial utiliza berkeley
, pero el nombre del alias de despliegue puede ser cualquiera y no tiene que coincidir con el nombre de la red.
URL de la API GraphQL de Mina: https://proxy.berkeley.minaexplorer.com/graphql
Tarifa de transacción a utilizar al desplegar: 0.1
Cuenta para pagar las tarifas de transacción: Crear un nuevo par de pagadores de tarifas
Cuando se te pida que elijas una cuenta para pagar las tarifas de transacción, selecciona usar una cuenta diferente:
03-deploy
:Tus pares de claves y el alias de despliegue se han creado:
Solicita fondos del Faucet de Testnet para financiar tu cuenta pagadora de tarifas.
Sigue las indicaciones para solicitar tMINA. Para este tutorial, tu dirección MINA se rellena en el Faucet de Testnet. tMINA llega a tu dirección cuando se produce el próximo bloque (aproximadamente 3 minutos).
Para desplegar tu proyecto:
En el mensaje interactivo, selecciona el alias de despliegue berkeley
:
Se genera una clave de verificación para tu contrato inteligente (tarda de 10 a 30 segundos).
Se muestra el proceso de despliegue:
Cuando se te pregunte, escribe yes
para confirmar y enviar la transacción.
Para ver la transacción zkApp, ve a https://berkeley.minaexplorer.com/transaction/<txn-hash>
, donde <txn-hash>
es el hash de la transacción que se muestra en tu terminal.
Después de que la transacción se incluya en un bloque, ¡tu contrato inteligente está desplegado!
Ejecutaste el comando zk config
para:
/Users/<username>/.cache/zkapp-cli/keys/03deploy.json
keys/berkeley.json
Solicitaste tMINA para financiar tus transacciones de despliegue. Usa el tMINA restante para seguir construyendo y probando.
Ejecutaste el comando zk deploy
para:
config.json
del proyecto¡Felicidades!
Debido a que este tutorial utilizó el contrato inteligente del Tutorial 1: Hola Mundo, los permisos de editState
del contrato inteligente requieren que una transacción contenga una prueba zk válida que haya sido creada por la clave privada asociada con esta cuenta zkApp.
Cuando cambias el código del contrato inteligente, la clave de verificación asociada también cambia. Utiliza los mismos pasos para volver a desplegar el contrato inteligente.
Para un contrato inteligente típico, los permisos se establecen para permitir solo la autorización por medio de prueba.
Mira este tutorial para obtener una guía paso a paso y explicaciones adicionales sobre cómo desplegar un zkApp.
Cómo crear un contrato inteligente
El video se proporciona con fines educativos y utiliza versiones anteriores de zkApp CLI y o1js, por lo que hay algunas diferencias. Este tutorial se ha probado con una versión específica de zkApp CLI y o1js.
¡Felicidades! Has desplegado con éxito un contrato inteligente en una red en vivo.