1. Crear un Smart Contract
Este es el primer capítulo del tutorial sobre construir una dapp end-to-end en Aptos. Si aún no lo has hecho, revisa esa introducción y asegúrate de que tu entorno cumpla con los prerrequisitos listados allí.
Ahora que tienes todo configurado, exploremos el directorio contract.

¿Qué es un archivo Move.toml?
Sección titulada «¿Qué es un archivo Move.toml?»Un archivo Move.toml es un archivo de manifiesto (manifest file) que contiene metadatos como el nombre, la versión y las dependencias del paquete.
Echa un vistazo al nuevo archivo Move.toml. Deberías ver la información de tu paquete y una dependencia de AptosFramework. La dependencia AptosFramework apunta a la rama main del repositorio de GitHub aptos-core/aptos-move/framework/aptos-framework.
¿Por qué un directorio sources?
Sección titulada «¿Por qué un directorio sources?»El directorio sources contiene una colección de archivos de módulos .move. Más adelante, cuando queramos compilar el paquete usando la CLI, el compilador buscará ese directorio sources y su archivo Move.toml.
¿Qué es el directorio tests?
Sección titulada «¿Qué es el directorio tests?»El directorio tests contiene archivos .move que se utilizan para testear los archivos en nuestro directorio sources.
Crear un módulo Move
Sección titulada «Crear un módulo Move»Se necesita una cuenta para publicar (hacer publish) un módulo Move. Cuando instalamos la plantilla, la herramienta creó una nueva cuenta para nosotros y la agregó al archivo .env.
Si abres ese archivo, verás un contenido similar a este:
PROJECT_NAME=my-first-dappVITE_APP_NETWORK=devnetVITE_APTOS_API_KEY=YOUR_API_KEYVITE_MODULE_PUBLISHER_ACCOUNT_ADDRESS=0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb#This is the module publisher account's private key. Be cautious about who you share it with, and ensure it is not exposed when deploying your dApp.VITE_MODULE_PUBLISHER_ACCOUNT_PRIVATE_KEY=0x84638fd5c42d0937503111a587307169842f355ab661b5253c01cfe389373f43La plantilla Boilerplate viene con un archivo message_board.move pregenerado, un archivo de test relevante y un archivo Move.toml. No usaremos message_board.move en este tutorial, así que elimínalo.
Como mencionamos, nuestro directorio sources contiene nuestros archivos de módulos .move; así que vamos a crear un nuevo archivo todolist.move.
-
Crea un nuevo archivo
todolist.movedentro del directoriosourcesy agrega lo siguiente a ese archivo:module todolist_addr::todolist {} -
Abre el archivo
Move.toml, reemplaza el nombre deMessageBoardaTodolisty la dirección demessage_board_addratodolist_addr, como en el siguiente código:[package]name = "Todolist"version = "1.0.0"authors = [][addresses]todolist_addr = "_"[dependencies]AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", rev = "mainnet", subdir = "aptos-framework" }[dev-dependencies]
¿Qué es el '_' en el archivo Move.toml?
Sección titulada «¿Qué es el '_' en el archivo Move.toml?»El '_' es un placeholder para la dirección de la cuenta. Cuando ejecutamos el compilador move, este reemplazará el marcador con la dirección de cuenta real.
Scripts para correr comandos move
Sección titulada «Scripts para correr comandos move»create-aptos-dapp viene con scripts prefabricados para correr comandos move fácilmente, como compile, test y publish.
- Abre cada uno de los archivos en el directorio
scripts/movey actualiza la variablemessage_board_addrpara que seatodolist_addr....namedAddresses: {todolist_addr: process.env.VITE_MODULE_PUBLISHER_ACCOUNT_ADDRESS,},...
Nuestra lógica del contrato
Sección titulada «Nuestra lógica del contrato»Antes de saltar a escribir código, primero entendamos qué queremos que haga nuestro programa de smart contract. Para facilitar la comprensión, mantendremos la lógica bastante simple:
- Una cuenta crea una nueva lista.
- Una cuenta crea una nueva tarea en su lista.
- Cada vez que alguien crea una nueva tarea, emitir un evento
TaskCreated.
- Cada vez que alguien crea una nueva tarea, emitir un evento
- Permitir que una cuenta marque su tarea como completada.
Podemos empezar definiendo un struct TodoList, que contiene:
- array de tareas (tasks)
- un contador de tareas que cuenta el número de tareas creadas (podemos usar esto para diferenciar entre las tareas)
Y también crear un struct Task que contiene:
task_id- derivado del contador de tareas de TodoList.creator_addr- la dirección de la cuenta que creó esa tarea.content- el contenido de la tarea.completed- un booleano que marca si esa tarea está completada o no.
En el archivo todolist.move, actualiza el contenido en el módulo con:
.../// Main resource that stores all tasks for an accountstruct TodoList has key { tasks: Table<u64, Task>, task_counter: u64}
/// Individual task structurestruct Task has store, drop, copy { task_id: u64, creator_addr: address, content: String, completed: bool,}...¿Qué acabamos de agregar?
TodoList
Un struct que tiene las abilities key y store:
- La ability
Keypermite que el struct se use como un identificador de almacenamiento. En otras palabras,keyes una capacidad para ser almacenado en el nivel superior (top-level) y actuar como almacenamiento. Lo necesitamos aquí para queTodoListsea un recurso (resource) almacenado en nuestra cuenta de usuario.
Cuando un struct tiene la ability key, convierte este struct en un resource:
- Un
Resourcese almacena bajo la cuenta; por lo tanto, existe solo cuando se asigna a una cuenta y puede ser accedido solo a través de esta cuenta.
Task
Un struct que tiene las abilities store, drop y copy.
• Store - Task necesita Store ya que se almacena dentro de otro struct (TodoList).
• Copy - el valor puede ser copiado (o clonado por valor).
• Drop - el valor puede ser droppeado (desechado) al final del scope.
Intentemos compilar lo que tenemos ahora (Spoiler alert: no va a funcionar):
-
Ejecuta:
npm run move:compile¿Ves errores? Entendámoslos.Tenemos algunos errores de
Unbound type(tipo no vinculado); esto sucede porque usamos algunos tipos pero nunca los importamos, y el compilador no sabe de dónde sacarlos.En la parte superior del módulo, importa esos tipos agregando:
...use aptos_std::table::Table;use std::string::String;...Eso le dirá al compilador de dónde puede obtener esos tipos.
-
Corre el comando
npm run move:compilenuevamente; si todo sale bien, deberías ver una respuesta similar a esta (donde la dirección de cuenta resultante es la dirección de tu cuenta de perfil predeterminada):Ventana de terminal Compiling, may take a little while to download git dependencies...UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.gitINCLUDING DEPENDENCY AptosFrameworkINCLUDING DEPENDENCY AptosStdlibINCLUDING DEPENDENCY MoveStdlibBUILDING Todolist{"Result": ["1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist"]}En este punto, hemos compilado exitosamente nuestro módulo Move. ¡Genial!
También tenemos un nuevo directorio move/build (creado por el compilador) que contiene nuestros módulos compilados, información de build y el directorio sources.
Función create list
Sección titulada «Función create list»Lo primero que una cuenta puede y debe hacer con nuestro contrato es crear una nueva lista.
Crear una lista es esencialmente enviar una transacción, por lo que necesitamos conocer al signer (firmante) que firmó y envió la transacción:
-
Agrega una función
Notesque acepte unsignerdentro del móduloTodolist.public entry fun create_list(account: &signer){}Entendamos los componentes de esta función
-
entry- una función entry es una función que puede ser llamada a través de transacciones. En pocas palabras, cada vez que quieras enviar una transacción a la blockchain, debes llamar a una función entry. -
&signer- El argumento signer es inyectado por la Move VM como la dirección que firmó esa transacción.
Nuestro código tiene un recurso
TodoList. El recurso se almacena bajo la cuenta; por lo tanto, existe solo cuando se asigna a una cuenta y puede ser accedido solo a través de esta cuenta.Eso significa que para crear el recurso
TodoList, necesitamos asignarlo a una cuenta para que solo esta cuenta pueda tener acceso a él.La función
Notespuede manejar esa creación del recursoTodoList. -
-
Agrega lo siguiente a la función
Notes:/// Initializes a new todo list for the accountpublic entry fun create_list(account: &signer) {let tasks_holder = TodoList {tasks: table::new(),task_counter: 0};// Move the TodoList resource under the signer accountmove_to(account, tasks_holder);}Esta función toma un
signer, crea un nuevo recursoTodoListy usamove_topara almacenar el recurso en la cuenta del signer proporcionado. -
Asegurémonos de que todo siga funcionando corriendo el comando
npm run move:compilenuevamente.
Función create task
Sección titulada «Función create task»Como se mencionó antes, nuestro contrato tiene una función de crear tarea que permite a una cuenta crear una nueva task. Crear una task también es esencialmente enviar una transacción, por lo que necesitamos conocer al signer que firmó y envió la transacción. Otro elemento que queremos aceptar en nuestra función es el content (contenido) de la tarea.
-
Agrega una función
create_taskque acepte unsigner, elcontentde la tarea y la lógica de la función./// Creates a new task in the todo listpublic entry fun create_task(account: &signer, content: String) acquires TodoList {// Get the signer addresslet signer_address = signer::address_of(account);// Get the TodoList resourcelet todo_list = borrow_global_mut<TodoList>(signer_address);// Increment task counterlet counter = todo_list.task_counter + 1;// Create a new tasklet new_task = Task {task_id: counter,creator_addr: signer_address,content,completed: false};// Add the new task to the tasks tabletodo_list.tasks.upsert(counter, new_task);// Update the task countertodo_list.task_counter = counter;// Emit a task created eventevent::emit(TaskCreated {task_id: counter,creator_addr: signer_address,content,completed: false})} -
Notarás que aún no hemos creado el struct del evento
TaskCreated. Créalo en la parte superior del archivo (debajo de las declaracionesuse) con el siguiente código:#[event]struct TaskCreated has drop, store {task_id: u64,creator_addr: address,content: String,completed: bool,} -
Dado que ahora usamos tres nuevos módulos: signer, event y table (puedes ver que se usan en
signer::,event::ytable::), necesitamos importar estos módulos. En la parte superior del archivo, agrega esas dos declaracionesuse(reemplaza la declaraciónusedetablecon el siguiente código):use aptos_framework::event;use aptos_std::table::{Self, Table}; // This one we already have, need to modify ituse std::signer; -
Asegurémonos de que todo siga funcionando corriendo el comando
npm run move:compilenuevamente.
Volviendo al código; ¿qué está pasando aquí?
- Primero, queremos obtener la dirección del signer, para poder obtener el recurso
TodoListde esta cuenta. - Luego, recuperamos el recurso
TodoListcon lasigner_address; con eso tenemos acceso a las propiedades deTodoList. - Ahora podemos incrementar la propiedad
task_countery crear una nuevaTaskcon lasigner_address, elcountery elcontentproporcionado. - Lo pusheamos a la tabla
todo_list.tasksque contiene todas nuestras tareas junto con el nuevocounter(que es la key de la tabla) y la Task recién creada. - Luego asignamos el
task_counterglobal para que sea el nuevo contador incrementado. - Finalmente, emitimos el evento
TaskCreatedque contiene los datos de la nueva Task.event::emit()es una función deaptos-frameworkque emite un evento de módulo con payloadmsg. En nuestro caso, le estamos pasando a la función un struct de eventoTaskCreatedcon los datos de la nueva Task.
Función complete task
Sección titulada «Función complete task»Otra función que queremos que tenga nuestro contrato es la opción de marcar una tarea como completada.
-
Agrega una función
complete_taskque acepte unsignery untask_id:/// Marks a task as completedpublic entry fun complete_task(account: &signer, task_id: u64) acquires TodoList {// Get the signer addresslet signer_address = signer::address_of(account);// Get the TodoList resourcelet todo_list = borrow_global_mut<TodoList>(signer_address);// Get the task recordlet task_record = todo_list.tasks.borrow_mut(task_id);// Mark the task as completedtask_record.completed = true;}Entendamos el código.
- Como antes en nuestra función create list, recuperamos el struct
TodoListmediante la dirección del signer, para poder tener acceso a la tabla de tareas que contiene todas las tareas de la cuenta. - Luego, obtenemos una referencia mutable para la tarea con el
task_idproporcionado en la tablatodo_list.tasks. - Finalmente, actualizamos la propiedad
completedde esa tarea para que sea true.
- Como antes en nuestra función create list, recuperamos el struct
-
Ahora compila el código ejecutando:
npm run move:compilepara asegurarte de que todo siga funcionando.
Agregar validaciones
Sección titulada «Agregar validaciones»Ya que este código compila, queremos tener algunas validaciones y comprobaciones antes de crear una nueva tarea o actualizar la tarea como completada, para poder estar seguros de que nuestras funciones trabajan como se espera.
-
Agrega una comprobación a la función
create_taskpara asegurarte de que la cuenta del signer tenga una lista:public entry fun create_task(account: &signer, content: String) acquires TodoList {// gets the signer addresslet signer_address = signer::address_of(account);// assert signer has created a listassert!(exists<TodoList>(signer_address), 1);...} -
Agrega una comprobación a la función
complete_taskpara asegurarte de que:- el signer haya creado una lista.
- la tarea exista.
- la tarea no esté completada.
Con el siguiente código:
/// Marks a task as completedpublic entry fun complete_task(account: &signer, task_id: u64) acquires TodoList {// Get the signer addresslet signer_address = signer::address_of(account);// Ensure the account has initialized a todo listassert!(exists<TodoList>(signer_address), 1);// Get the TodoList resourcelet todo_list = borrow_global_mut<TodoList>(signer_address);// Ensure the task existsassert!(todo_list.tasks.contains(task_id), 2);// Get the task recordlet task_record = todo_list.tasks.borrow_mut(task_id);// Ensure the task is not already completedassert!(task_record.completed == false, 3);// Mark the task as completedtask_record.completed = true;}
¡Acabamos de agregar nuestros primeros statements assert!
Si te diste cuenta, assert acepta dos argumentos: el primero es qué comprobar, y el segundo es un código de error. En lugar de pasar un número arbitrario, una convención es declarar errors en la parte superior del archivo del módulo y usarlos en su lugar.
En la parte superior del archivo del módulo (debajo de los statements use), agrega esas declaraciones de error:
// Errors/// Account has not initialized a todo listconst ENOT_INITIALIZED: u64 = 1;/// Task does not existconst ETASK_DOESNT_EXIST: u64 = 2;/// Task is already completedconst ETASK_IS_COMPLETED: u64 = 3;Ahora podemos actualizar nuestros asserts con estas constantes:
/// Creates a new task in the todo listpublic entry fun create_task(account: &signer, content: String) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
...}
/// Marks a task as completedpublic entry fun complete_task(account: &signer, task_id: u64) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Ensure the task exists assert!(todo_list.tasks.contains(task_id), ETASK_DOESNT_EXIST);
// Get the task record let task_record = todo_list.tasks.borrow_mut(task_id);
// Ensure the task is not already completed assert!(task_record.completed == false, ETASK_IS_COMPLETED);
// Mark the task as completed task_record.completed = true;}¡MARAVILLOSO!
Detengámonos un momento y asegurémonos de que nuestro código compila corriendo el comando npm run move:compile. Si todo sale bien, deberíamos tener una salida similar a:
Compiling, may take a little while to download git dependencies...UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.gitINCLUDING DEPENDENCY AptosFrameworkINCLUDING DEPENDENCY AptosStdlibINCLUDING DEPENDENCY MoveStdlibBUILDING MessageBoard{ "Result": [ "1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist" ]}Si encuentras errores, asegúrate de haber seguido los pasos anteriores correctamente e intenta determinar la causa de los problemas.
Escribir tests
Sección titulada «Escribir tests»Ahora que tenemos lista la lógica de nuestro smart contract, necesitamos agregar algunos tests.
Primero, elimina el archivo test_end_to_end.move en el directorio tests, ya que no lo usaremos.
-
Por simplicidad, y porque no tenemos mucho código que probar, tendremos los tests en el archivo
todolist.move. Si necesitas escribir un test más complejo, deberías crear un archivo de test separado en el directoriotests.Los pasos del test son:
// create a list// create a task// update task as completed -
Agrega el siguiente código al final del archivo
todolist.move:#[test]public entry fun test_flow() {}Nota: Las funciones de test usan la anotación
#[test]. -
Actualiza la función de test para que sea:
#[test(admin = @0x123)]public entry fun test_flow(admin: signer) acquires TodoList {// Create an admin account for testingaccount::create_account_for_test(signer::address_of(&admin));// Initialize a todo list for the admin accountcreate_list(&admin);// Create a task and verify it was added correctlycreate_task(&admin, string::utf8(b"Create e2e guide video for aptos devs."));let todo_list = borrow_global<TodoList>(signer::address_of(&admin));assert!(todo_list.task_counter == 1, 5);// Verify task detailslet task_record = todo_list.tasks.borrow(todo_list.task_counter);assert!(task_record.task_id == 1, 6);assert!(task_record.completed == false, 7);assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 8);assert!(task_record.creator_addr == signer::address_of(&admin), 9);// Complete the task and verify it was marked as completedcomplete_task(&admin, 1);let todo_list = borrow_global<TodoList>(signer::address_of(&admin));let task_record = todo_list.tasks.borrow(1);assert!(task_record.task_id == 1, 10);assert!(task_record.completed == true, 11);assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 12);assert!(task_record.creator_addr == signer::address_of(&admin), 13);}Nuestra anotación
#[test]ha cambiado y declara una variable de cuenta.Además, la función en sí misma ahora acepta un argumento signer.
Entendamos nuestros tests.
Dado que nuestros tests corren fuera del scope de una cuenta, necesitamos crear cuentas para usar en nuestros tests. La anotación
#[test]nos da la opción de declarar esas cuentas. Usamos una cuentaadminy la configuramos con una dirección de cuenta aleatoria (@0x123). La función acepta este signer (cuenta) y lo crea usando una función integrada para crear una cuenta para test.Luego simplemente pasamos por el flujo:
- creando una lista
- creando una tarea
- actualizando una tarea como completada
Y hacemos assert de los datos/comportamiento esperados en cada paso.
Antes de correr los tests nuevamente, necesitamos importar (
use) algunos módulos nuevos que ahora estamos empleando en nuestro código: -
En la parte superior del archivo, agrega estos statements
use:#[test_only]use aptos_framework::account;#[test_only]use std::string::{Self};Nota que estamos usando la anotación
#[test_only]para importar los módulos solo para testing. Esto es porque no queremos usar estos módulos en nuestro código de producción. -
Corre el comando
npm run move:test. Si todo sale bien, deberíamos ver un mensaje de éxito como:Running Move unit tests[ PASS ] 0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist::test_flowTest result: OK. Total tests: 1; passed: 1; failed: 0{"Result": "Success"} -
Agreguemos un test más para asegurarnos de que nuestra función
complete_taskfuncione como se espera. Agrega otra función de test con:#[test(admin = @0x123)]#[expected_failure(abort_code = ENOT_INITIALIZED)]public entry fun account_can_not_update_task(admin: signer) acquires TodoList {// Create an admin account for testingaccount::create_account_for_test(signer::address_of(&admin));// Attempt to complete a task without creating a list first (should fail)complete_task(&admin, 2);}Este test confirma que una cuenta no puede usar esa función si no ha creado una lista antes.
El test también usa una anotación especial
#[expected_failure]que, como su nombre sugiere, espera fallar con un código de errorENOT_INITIALIZED. -
Corre el comando
npm run move:test. Si todo sale bien, deberíamos ver un mensaje de éxito como:Ventana de terminal Running Move unit tests[ PASS ] 0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist::account_can_not_update_task[ PASS ] 0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist::test_flowTest result: OK. Total tests: 2; passed: 2; failed: 0{"Result": "Success"}
Ahora que todo funciona, podemos compilar los módulos Move y publicar el paquete Move en la cadena (on-chain) para que nuestra app React (y todos los demás) puedan interactuar con nuestro smart contract.
Publicar el módulo todolist en la cadena
Sección titulada «Publicar el módulo todolist en la cadena»- Ejecuta:
npm run move:testynpm run move:compile- todo debería funcionar sin errores. - Ejecuta:
npm run move:publish - Ingresa
yesen el promptDo you want to publish this package at object address 0x8f66343d40de3eeef5dd45cab8c1531a542f0e5f546da9f11852d4c2b30165a7 [yes/no] >. (Spoiler alert: va a fallar)
¡Oh no! ¡Tuvimos un error!
Se queja de un mismatch (desajuste) de cuenta con el código de error MODULE_ADDRESS_DOES_NOT_MATCH_SENDER. Aparentemente compilamos el paquete con una cuenta diferente a la que intentamos usar para publicarlo.
Vamos a arreglarlo.
- Abre el archivo
scripts/move/publish.js. - Actualiza el valor de la variable
addressNamepara que seatodolist_addr.
Eso usará la misma cuenta que usamos para compilar el paquete.
Intentemos de nuevo:
-
Ejecuta:
npm run move:publish -
Ingresa
yesen el prompt. -
Ingresa
yesen el segundo prompt. -
Eso compilará, simulará y finalmente hará publish de tu módulo en devnet. Deberías ver un mensaje de éxito:
Ventana de terminal Transaction submitted: https://explorer.aptoslabs.com/txn/0x68dadf24b9ec29b9c32bd78836d20032de615bbef5f10db580228577f7ca945a?network=devnetCode was successfully deployed to object address 0x2bce4f7bb8a67641875ba5076850d2154eb9621b0c021982bdcd80731279efa6{"Result": "Success"} -
Ahora puedes dirigirte al enlace del Aptos Explorer y ver los detalles de la transacción. También puedes ver el módulo publicado on-chain buscando la object address.
Código completo del módulo Todolist
Sección titulada «Código completo del módulo Todolist»Aquí está el archivo todolist.move completo para confirmar tu trabajo:
module todolist_addr::todolist { use aptos_framework::event; use aptos_std::table::{Self, Table}; use std::signer; use std::string::String;
#[test_only] use aptos_framework::account; #[test_only] use std::string::{Self};
// Errors /// Account has not initialized a todo list const ENOT_INITIALIZED: u64 = 1; /// Task does not exist const ETASK_DOESNT_EXIST: u64 = 2; /// Task is already completed const ETASK_IS_COMPLETED: u64 = 3;
#[event] struct TaskCreated has drop, store { task_id: u64, creator_addr: address, content: String, completed: bool, }
/// Main resource that stores all tasks for an account struct TodoList has key { tasks: Table<u64, Task>, task_counter: u64 }
/// Individual task structure struct Task has store, drop, copy { task_id: u64, creator_addr: address, content: String, completed: bool, }
/// Initializes a new todo list for the account public entry fun create_list(account: &signer) { let tasks_holder = TodoList { tasks: table::new(), task_counter: 0 }; // Move the TodoList resource under the signer account move_to(account, tasks_holder); }
/// Creates a new task in the todo list public entry fun create_task(account: &signer, content: String) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Increment task counter let counter = todo_list.task_counter + 1;
// Create a new task let new_task = Task { task_id: counter, creator_addr: signer_address, content, completed: false };
// Add the new task to the tasks table todo_list.tasks.upsert(counter, new_task);
// Update the task counter todo_list.task_counter = counter;
// Emit a task created event event::emit(TaskCreated { task_id: counter, creator_addr: signer_address, content, completed: false }) }
/// Marks a task as completed public entry fun complete_task(account: &signer, task_id: u64) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Ensure the task exists assert!(todo_list.tasks.contains(task_id), ETASK_DOESNT_EXIST);
// Get the task record let task_record = todo_list.tasks.borrow_mut(task_id);
// Ensure the task is not already completed assert!(task_record.completed == false, ETASK_IS_COMPLETED);
// Mark the task as completed task_record.completed = true; }
#[test(admin = @0x123)] public entry fun test_flow(admin: signer) acquires TodoList { // Create an admin account for testing account::create_account_for_test(signer::address_of(&admin));
// Initialize a todo list for the admin account create_list(&admin);
// Create a task and verify it was added correctly create_task(&admin, string::utf8(b"Create e2e guide video for aptos devs.")); let todo_list = borrow_global<TodoList>(signer::address_of(&admin)); assert!(todo_list.task_counter == 1, 5);
// Verify task details let task_record = todo_list.tasks.borrow(todo_list.task_counter); assert!(task_record.task_id == 1, 6); assert!(task_record.completed == false, 7); assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 8); assert!(task_record.creator_addr == signer::address_of(&admin), 9);
// Complete the task and verify it was marked as completed complete_task(&admin, 1); let todo_list = borrow_global<TodoList>(signer::address_of(&admin)); let task_record = todo_list.tasks.borrow(1); assert!(task_record.task_id == 1, 10); assert!(task_record.completed == true, 11); assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 12); assert!(task_record.creator_addr == signer::address_of(&admin), 13); }
#[test(admin = @0x123)] #[expected_failure(abort_code = ENOT_INITIALIZED)] public entry fun account_can_not_update_task(admin: signer) acquires TodoList { // Create an admin account for testing account::create_account_for_test(signer::address_of(&admin));
// Attempt to complete a task without creating a list first (should fail) complete_task(&admin, 2); }}Ahora vamos a configurar el frontend en el capítulo 2.