# DAO - Sistema de recompensas a miembros y bloqueo

WARNING

En la unidad anterior comenzamos con la creación de una DAO y pudimos codificar tanto la función constructora como un método para el ingreso de nuevos miembros. Hoy vamos a crear dos nuevos métodos, uno para recompensar a los afiliados y otra para banear/bloquear afiliados.

# Análisis

Problema/Análisis: Nos gustaría que nuestra DAO pudiera otorgar puntos a los miembros a cambio de XRD, para que la DAO tenga liquidez. Estos puntos equivaldrán 1 a 1 con XRD, la idea es que un miembro pueda añadir liquidez al DAO y sean recompensado con tantos puntos como XRD haya añadido al tesoro de la DAO. Además nos gustaría que miembros con muchos puntos, al menos 1000, tengan el poder de bloquear a otros afiliados.

# Diseño

Diagramas

Al menos dos nuevos métodos: uno para recoger los XRD, guardarlos en el tesoro del DAO y seguidamente convertir esos XRD en puntos que guardaremos dentro del NFT. Y otro método para permitir a aquellos afiliados que tengan más de 10000 puntos puedan sacar tarjetas rojas (strikes) a un afiliado y cuando este tenga al menos 3 faltas será marcado como bloqueado.

# Programación

# NFT Estructura Datos:

Con el fín de guardar en los Nft los puntos obtenidos gracias a las aportaciones y saber si esta bloqueado o no, incluiremos en la estructura de los datos del Nft dos nuevos campos mutables de la siguiente manera:

#[derive(NonFungibleData)]
struct DatosAfiliado {
    nombre: String,
    #[scrypto(mutable)]
    puntos: Decimal,
    #[scrypto(mutable)]
    bloqueado: bool
}

Para declarar que un dato es mutable en el NFT solo hemos de agregar antes del dato el siguiente macro:

 #[scrypto(mutable)]

Esto nos permite, teniendo los permisos adecuados, modificar esos campos del no fungible.

# La estructura:

struct SistemaAfiliacion  {
    num_afiliado: u32,
    def_afiliado_nft: ResourceAddress,
    minteador: Vault,
    aportaciones: Vault
}

En este caso en la estructura del componente vamos a agregar un nuevo contenedor permanente, Vault, para guardar las aportaciones de los afiliados que finalmente se convertirán en el tesoro del DAO.

# Función constructora new

Al cambiar la estructura del componente es necesario también retocar la función constructora, en este caso debemos especificar que el contenedor aportaciones solo podrá recibir Radix Token.

aportaciones: Vault::new(RADIX_TOKEN)

Para poder cambiar los datos que hemos agregado a la estructura del 'no fungible' DatosAfiliado y asignar reglas de autorización añadiremos el método updateable_non_fungible_data.

Indicando que para poder cambiar los datos requiere una insignia del tipo especificado gracias a su dirección: minteador.resource_address(). Finalmente indicamos que esta regla no se puede cambiar AccessRule::DenyAll.

TIP

Recuerda que a partir de la v0.7 Scrypto incorpora la nueva sintaxis para indicar si se puede o no cambiar la regla con: AccessRule::DenyAll (opens new window)

  .updateable_non_fungible_data(rule!(require(minteador.resource_address())), AccessRule::DenyAll)
pub fn new() -> ComponentAddress {
    let minteador: Bucket = ResourceBuilder::new_fungible()
        .metadata("name", "Autorización mintear nuevos NFT")
        .initial_supply(1);

    let def_afiliado = ResourceBuilder::new_non_fungible(NoFungibleIdType::U32)
        .metadata("name", "Afiliado DAO")
        .metadata("symbol", "DAO")
        .mintable(rule!(require(minteador.resource_address())), AccessRule::DenyAll)
        .updateable_non_fungible_data(rule!(require(minteador.resource_address())), AccessRule::DenyAll)
        .no_initial_supply();

    Self {
        minteador: Vault::with_bucket(minteador),
        def_afiliado_nft: def_afiliado,
        num_afiliado: 0u32,
        aportaciones: Vault::new(RADIX_TOKEN)
    }
    .instantiate().globalize()
}

# Método aportaciones

pub fn aportaciones(&mut self, aportacion: Bucket, nft_dao: Proof) {
    let nft_proof = nft_dao.validate_proof(self.def_afiliado_nft).expect("Este no es el NFT de la DAO");
    let nft_id: NonFungibleId = nft_proof.non_fungible::<DatosAfiliado>().id();
    let mut nft_data: DatosAfiliado = nft_proof.non_fungible::<DatosAfiliado>().data();
    assert!(!nft_data.bloqueado, "¡Estas bloqueado en la DAO!");
    
    let puntos: Decimal = aportacion.amount();
    self.aportaciones.put(aportacion);

    nft_data.puntos += puntos;

    self.minteador.authorize(|| {
        borrow_resource_manager!(self.def_afiliado_nft).update_non_fungible_data(&nft_id, nft_data)
    });

    info!("Gracias por la aportación,  has recibido {} puntos en el DAO", puntos);
}

En este método los usuarios, en este caso los afiliados al DAO, podrán realizar sus aportaciones a la DAO ganando a cambio puntos del DAO equivalentes al mismo número de aportaciones en XRD. Para ello al método le pasamos dos parámetros: por un lado la prueba, Proof, de que la persona que accede es afiliado del DAO con su 'no fungible' y por otro lado un contenedor temporal, Bucket, que contiene la aportación en XRD.

Proof

  • Proof: es una prueba de que poseemos un recurso en cuestión. Gracias a este tipo de prueba no tenemos que pasar el recurso al contrato, solo la prueba de que lo poseemos. Piense en ello como mostrar una insignia en el mundo real. A quien sea que se lo muestres puede ver que lo posees y puede inspeccionarlo, pero en realidad no se lo estás entregando para que no puedan quedarse con él.

Ojo: En este caso estamos pasando una prueba de manera directa y explicita (passing by intent). Normalmente estas Proof o pruebas las vamos a tener en la La Zona de Autorización dentro de los manifiestos de transacciones, no te preocupes de todo esto hablaremos mas adelante!!!!

Antes de nada vamos a cerciorarnos de que los recursos que nos han pasado en la llamada al método son los apropiados, aunque fallaría más adelante y no permitiría finalizarse la llamada, es preferible controlar siempre los posibles errores para lanzar mensajes que puedan ayudar al usuario:

TIP

assert!: este macro de Rust rompe el fluejo de la función si la sentencia es false y devuelve un texto definido por nosotros.

Verificamos que la dirección del la Proof que nos pasan se la misma que hemos guardado. Utilizmos el método validate_proof y le pasamos el argumento de la dirección guaradada en el componente. Si la prueba es correcta entonces guardamos los datos del no fungible en una variable, si no devolvemos un mensaje de error.

let nft_proof = nft_dao.validate_proof(self.def_afiliado_nft).expect("Este no es el NFT de la DAO");

Una vez hemos garantizado que el no fungible es el que esperamos, ahora pasamos a guardar los datos de este en dos partes, por un lado el identificador y por otro los datos. Aquí nos aseguramos que nos han pasado solo una insignia como prueba, o al menos una de ellas. Declaramos como mutable la variable que contendra los datos de la estructura del nft porque luego vamos a modificarla.

let nft_id: NonFungibleId = nft_proof.non_fungible::<DatosAfiliado>().id();
let mut nft_data: DatosAfiliado = nft_proof.non_fungible::<DatosAfiliado>().data();

Una vez hemos extraido los datos del nft vamos a verificar que el afiliado a la DAO no este bloqueado con la macro assert!

assert!(!nft_data.bloqueado, "¡Estas bloqueado en la DAO!");

Seguidamente guardamos el número de XRD que nos han mandado como aportacion en la variable puntos, utilizamos la función amount() (opens new window) que nos devuelve un tipo decimal con la cantidad de recurso en un backet o vault.

let puntos: Decimal = aportacion.amount();

TIP

  • Recuerda que la palabra clave let nos permite declarar una variable. Para que su valor pueda ser modificado recuerda en establecer la partícula mut.

Ahora que ya conocemos el número de puntos, vamos a guardar los XRD aportados en el tesoro (Vault) del DAO, para ello usamos la función *put() (opens new window) que literalmente toma, en este caso, toma el bucket (opens new window) aportacion y lo introduce en el vault (opens new window) aportaciones.

self.aportaciones.put(aportacion);

Actulizaremos los datos del nft agregando los puntos correpondientes a la aportación.

nft_datos.puntos += puntos;

Primero sumamos los puntos a los que ya tuviera en afiliado en el NFT con el operador de asignación aritmético +=.

TIP

borrow_resource_manager!: es una macro para simplificar el uso del ResourceManager

self.minteador.authorize(|| {
    borrow_resource_manager!(self.def_afiliado_nft).update_non_fungible_data(&nft_id, nft_data)
});

Y seguidamente actualizamos los datos del NFT con la función update_non_fungible_data() (opens new window) a la que le pasamos los siguientes argumentos: identificador del nft, datos actualizados. El procedimiento es parecido al acuñado. Para poder acceder a la actualizción de los datos necesitamos una autorización por eso envolvemos la instrucción en el método autorize.

TIP

Los Closures ||: Es una forma abriviada de llamar a una función.

Finalmente si todo el proceso ha sido exitoso lanzaremos el marco info!() (opens new window) para devolver un mensaje de logro.

info!("Gracias por la aportación,  has recibido {} puntos en el DAO", puntos);

# Método bloqueo

pub fn bloqueo(&mut self, id_bloqueo: NonFungibleId, nft_dao: Proof) {
    let nft_proof = nft_dao.validate_proof(self.def_afiliado_nft).expect("Este no es el NFT de la DAO");
    let nft_data: DatosAfiliado = nft_proof.non_fungible::<DatosAfiliado>().data();
    assert!(!nft_data.bloqueado, "¡Estas bloqueado en la DAO!");
    assert!(nft_data.puntos >= 1000.into(), "No tienes suficientes puntos para bloquear a otro afiliado");

    let mut nft_datos_bloqueo: DatosAfiliado = borrow_resource_manager!(self.def_afiliado_nft).get_non_fungible_data(&id_bloqueo);
    nft_datos_bloqueo.bloqueado = true;
    self.minteador.authorize(|| {
        borrow_resource_manager!(self.def_afiliado_nft).update_non_fungible_data(&id_bloqueo, nft_datos_bloqueo)
    });
}
pub fn bloqueo(&mut self, id_bloqueo: NonFungibleId, nft_dao: Proof)

Este método necesita como argumento, además del self o datos permanentes de la estructura esta vez mutables para poder realizar modificaciones, un tipo de dato NonFungibleId (opens new window) donde pasaremos al método el identificador del NFT que queremos bloquear y la prueba de que tenemos un NFT como afiliados al DAO.

let nft_proof = nft_dao.validate_proof(self.def_afiliado_nft).expect("Este no es el NFT de la DAO");
let nft_data: DatosAfiliado = nft_proof.non_fungible::<DatosAfiliado>().data();
assert!(!nft_data.bloqueado, "¡Estas bloqueado en la DAO!");

Al igual que en método anterior validamos el nft pasado como prueba y tomamos sus datos d. Esto lo hacemos, en este caso, primero para saber si es un usuario bloqueado y entonces no le dejaremos seguir:

assert!(!nft_data.bloqueado, "¡Estas bloqueado en la DAO!");

TIP

  • Para negar utilizamos !, es decir estamos preguntando si "¿no es true?"

y segundo cerciorarnos de que la persona posea, al menos, suficientes puntos de afiliado como para poder bloquear a otro:

assert!(nft_data.puntos >= 1000.into(), "No tienes suficientes puntos para bloquear a otro afiliado");

en ambos casos utilizamos el macro assert!() que ya vimos en el método anterior.

Si finalmente se han pasado todos los filtros mencionados anteriormente actualizaremos los datos del nft que se desea bloquear cambiando el campo de bloqueo a true:

let mut nft_datos_bloqueo: DatosAfiliado = borrow_resource_manager!(self.def_afiliado_nft).get_non_fungible_data(&id_bloqueo);
nft_datos_bloqueo.bloqueado = true;
self.minteador.authorize(|| {
    borrow_resource_manager!(self.def_afiliado_nft).update_non_fungible_data(&id_bloqueo, nft_datos_bloqueo)
});

Este procedimiento es parecido de actualización de los datos mutables de un nft es parecido al que ya utilizamos en el método anterior. Reseñar que podemos hacerlo ya que poseemos la autorización del minteador que se guarda en el propio componente para realizar estas acciones.

# Código completo:

use scrypto::prelude::*;

#[derive(NonFungibleData)]
struct DatosAfiliado {
    nombre: String,
    #[scrypto(mutable)]
    puntos: Decimal,
    #[scrypto(mutable)]
    bloqueado: bool
}

blueprint! {
    struct SistemaAfiliacion  {
        num_afiliado: u32,
        def_afiliado_nft: ResourceAddress,
        minteador: Vault,
        aportaciones: Vault
    }

    impl SistemaAfiliacion  {
        
        pub fn new() -> ComponentAddress {
            let minteador: Bucket = ResourceBuilder::new_fungible()
                .metadata("name", "Autorización mintear nuevos NFT")
                .initial_supply(1);

            let def_afiliado = ResourceBuilder::new_non_fungible(NonFungibleIdType::U32)
                .metadata("name", "Afiliado DAO")
                .metadata("symbol", "DAO")
                .mintable(rule!(require(minteador.resource_address())), AccessRule::DenyAll)
                .updateable_non_fungible_data(rule!(require(minteador.resource_address())), AccessRule::DenyAll)
                .no_initial_supply();

            Self {
                minteador: Vault::with_bucket(minteador),
                def_afiliado_nft: def_afiliado,
                num_afiliado: 0u32,
                aportaciones: Vault::new(RADIX_TOKEN)
            }
            .instantiate().globalize()
        }

        
        pub fn afiliarse_dao(&mut self, nombre: String) -> Bucket {
            self.num_afiliado += 1;
            let id: NonFungibleId = NonFungibleId::U32(self.num_afiliado);

            let data: DatosAfiliado = DatosAfiliado {nombre, puntos: Decimal::zero(), bloqueado: false};

            self.minteador.authorize(|| {
                borrow_resource_manager!(self.def_afiliado_nft).mint_non_fungible(&id, data)
            })
        }
        
        pub fn aportaciones(&mut self, aportacion: Bucket, nft_dao: Proof) {
            let nft_proof = nft_dao.validate_proof(self.def_afiliado_nft).expect("Este no es el NFT de la DAO");
            let mut nft_data: DatosAfiliado = nft_proof.non_fungible::<DatosAfiliado>().data();
            assert!(!nft_data.bloqueado, "¡Estas bloqueado en la DAO!");
            
            let puntos: Decimal = aportacion.amount();
            self.aportaciones.put(aportacion);

            nft_data.puntos += puntos;

            self.minteador.authorize(|| {
               borrow_resource_manager!(self.def_afiliado_nft).update_non_fungible_data(nft_proof.non_fungible::<DatosAfiliado>().id(), nft_data)
            });

            info!("Gracias por la aportación,  has recibido {} puntos en el DAO", puntos);
        }

        pub fn bloqueo(&mut self, id_bloqueo: NonFungibleId, nft_dao: Proof) {
            let nft_proof = nft_dao.validate_proof(self.def_afiliado_nft).expect("Este no es el NFT de la DAO");
            //let nft_id: NonFungibleId = nft_proof.non_fungible::<DatosAfiliado>().id();
            let nft_data: DatosAfiliado = nft_proof.non_fungible::<DatosAfiliado>().data();
            assert!(!nft_data.bloqueado, "¡Estas bloqueado en la DAO!");
            assert!(nft_data.puntos >= 1000.into(), "No tienes suficientes puntos para bloquear a otro afiliado");

            let mut nft_datos_bloqueo: DatosAfiliado = borrow_resource_manager!(self.def_afiliado_nft).get_non_fungible_data(&id_bloqueo);
            nft_datos_bloqueo.bloqueado = true;
            self.minteador.authorize(|| {
                borrow_resource_manager!(self.def_afiliado_nft).update_non_fungible_data(&id_bloqueo, nft_datos_bloqueo)
            });
        }
    }
}

# Compilación y ejecución

A estas alturas seguro que ya sabes publicar el package, instanciar el component y llamar a las funciones pasando un parámetro.

Recuerda

  • Puedes sobre-escribir un package y modificarlo sin necesidad de cambiar de dirección del package:
resim publish . --address $package 

En esta ocasión mencionaros que para realizar pruebas y ejecutar el componente deberíais crear más de una cuenta, esto es sencillo solo hay que ejecutar de nuevo la instrucción:

resim new-account

Y guardarla en una nueva variable por ejemplo:

set account2 [numero de cuenta]

Recuerda guardar también la información de la cuenta como la clave privada, la dirección de sus XRD, la dirección y numero del NFT de afiliado para despues utilizarlo.

Ejemplo:

Resources:
├─ { amount: 1, resource address: resource_sim1qz8d74vzsjllapmy0zpy58w2ecrnnvtfxvhwqpcdexeq6qquts, name: "Afiliado DAO", symbol: "DAO" }
│  └─ NonFungible { id: NonFungibleId("0902000000"), immutable_data: Struct("Antonio Mister BTC"), mutable_data: Struct(Decimal("1000"), false) }
└─ { amount: 0, resource address: resource_sim1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqu57yag, name: "Radix", symbol: "XRD" }

Para cambiar de una cuenta a otra dentro del simulador has de ejecutar la siguiente instrucción:

resim set-default-account [account2_address] [account2_privateKey]

Esta instrucción establece una cuenta como principal (o por defecto), y no solamente necesitaremos la dirección sino también la llave pública (privatekey) la cual podemos conseguir en el momento de crear la cuenta:

PS C:\Users\Andres\radixdlt-scrypto\dao> resim new-account
A new account has been created!
Account address: 022f189e2f5d2ebf08d5d9d9c0ed0503748b8922bbf1a86c18b5c2
Public key: 007902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451

Y guardala como hicimos hasta ahora:

set pubkey2 04007902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451
Solo para aquellos (tipo Emilio 🤪) que no quieren pensar!!!
  1. Limpiar el simulador
resim reset
  1. Crear un Package
scrypto new-package dao_mejorado
  1. Crear una cuenta (recuerda copiar la dirección de los XRD de tu cuenta)
resim new-account
set acct [Address de la cuenta que acabamos de crear]
set privatekey [Guardamos la clave privada de esta cuenta]
resim new-simple-badge
set badge [NFAddress]
  1. Copiar o escribir el código (recuerda guardar ctrl + s)
  • Recuerda guardar el código de este ejercicio dentro del archivo lib.rs que has creado en la carpeta \radixdlt-scrypto\dao\src\lib.rs
  1. Publicar y guardamos la dirección del Package
//resim publish . --owner-badge <owner_NFAddress>
resim publish . --owner-badge $badge
set pack [New Package Reference]
  1. Instanciar componente (recuerda que en este caso hay que añadir el argumento del precio) y guardar la dirección del componente.
resim call-function $pack SistemaAfiliacion new
set comp [dirección del componente]
  1. Probar método afiliarse_dao
resim call-method $comp afiliarse_dao "Emilio Bitcoin"
// Ojo que cuando pasamos un dato de tipo string y tiene mas de una palabra debemos entrecomillarlo.
  1. Comprobar el nft de afiliado y guardar datos
resim show $acct
  1. Guardamos la definición del xrd para realizar aportaciones
resim show $acct
set xrd 030000000000000000000000000000000000000000000000000004
  1. Mandamos aportaciones al DAO que se convertirán en puntos dentro del nft
resim call-method $comp aportaciones 1000,$xrd 1,0399d3f4678fbf0ec6abb57bb17af7ddcc48ce1370e65eb99f8e13
// recuerda que para pasar una autorización lo hacemos al final, donde 1 es la unidad a pasar el resto es la definición del non fungible
  1. Vemos los cambios en nuestro nft
resim show $acct
  1. Para probar el bloqueo podemos crear una nueva cuenta y definirla por defecto para usarla
resim new-account
set acct2 xxxxxxxxxxxxxxxx
set privatekey2 xxxxxxxxxxxxxx
resim set-default-account $acct2 $privatekey2
resim call-method $comp afiliarse_dao "Antonio Mister BTC"
resim show $acct2
set id2=00000000000000000000000000000002
  1. Volvemos a definir $acct como cuenta por defecto para poder bloquear $acct2
resim set-default-account $acct $privatekey
resim call-method $comp bloqueo $id2 1,xxxxxxxxxxxxxxxxxxxxxx
resim show $acct2

😎 ¿Qué tal?, ¡seguro no fue facil llegar hasta aquí! pero sabes ya sabes muchas muchas cosas que antes ni imaginabas. ¡Has podido crear una DAO! cierto, es muy sencilla pero el límite lo pones tú. En pocos unidades más habrás terminado el nivel básico y seras todo un desarrollador Junior de contratos inteligentes con Scrypto. No te rindas...!!!