馃彚 Universidad de Huelva (UHU)
馃搯 Curso 2020-2021
馃帗 Modelos Avanzados de Computaci贸n
Ihar Myshkevich (@IgorMy)
V铆ctor M. Rodr铆guez (@VictorNarov)
En este repositorio se documentar谩 la implementaci贸n de un Smart Contract codificado en lenguaje Haskell sobre plataforma Plutus Playground. En este, se visualiza c贸mo una cartera genera un contrato de transferencia de capital (Ada) y c贸mo la carpeta destinataria recoge ese capital.
En este apartado veremos las diferentes partes de este Smart Contract.
import Playground.Contract -- Gesti贸n de contratos en el entorno Plutus Playground.
import Control.Monad (void) -- Funci贸nes de c谩lculo avanzado.
import Data.Aeson (FromJSON, ToJSON) -- Tipos y funciones para trabajar eficazmente con datos JSON.
import qualified Data.Text as T -- Tipos y funciones para trabajar eficazmente con texto plano.
import GHC.Generics (Generic) -- Funciones para la conversi贸n de datos.
import Language.Plutus.Contract -- Contratos Pulutus.
import qualified Language.PlutusTx as PlutusTx -- Bibliotecas y el compilador para compilar Haskell en Plutus.
import Language.PlutusTx.Prelude -- Sustituto del Haskell prelude que funciona mejor con PlutusTx.
import Ledger -- Contenedor para almacenar datos en bruto de una forma m谩s eficiente.
import qualified Ledger.Ada as Ada -- Almacenamiento de tipo Ada (moneda).
import qualified Ledger.Constraints as Constraints -- Restricciones del almacenamiento.
import qualified Ledger.Typed.Scripts as Scripts -- Funciones del almacenamiento.
import Schema -- Bilioteca Haskell para serializar y deserializar datos en JSON.
import Wallet.Emulator.Wallet -- Biblioteca Haskell para gestionar carteras virtuales.
En este apatado importamos las librer铆as de CARDANO necesarias para la ejecuci贸n del Smart Contract. Su funcionalidad est谩 comentada a la derecha de su declaraci贸n.
data SmartContractData =
SmartContractData
{ recipient :: PubKeyHash
, amount :: Ada
}
deriving stock (Show, Generic)
PlutusTx.makeIsData ''SmartContractData
PlutusTx.makeLift ''SmartContractData
SmartContractData describe el destinatario al que se le va a enviar el capital y la cantidad de capital en Ada. Estamos utilizando el tipo PubKeyHash para identificar al destinatario. Al realizar el pago podemos utilizar el hash para crear la salida de clave p煤blica.
validateSmartContract :: SmartContractData -> () -> ValidatorCtx -> Bool
validateSmartContract SmartContractData{recipient, amount} _ ValidatorCtx{valCtxTxInfo} =
Ada.fromValue (valuePaidTo valCtxTxInfo recipient) == amount
Esta funci贸n es muy importante. Su misi贸n es tomar ambas transacciones por separado y decidir si son v谩lidas. Solo en ese caso se ejecuta y se cierra el contrato. En nuestro caso, este script comprueba que la cantidad que recibir谩 el destinatario es la acordada por ambas partes.
data LockArgs =
LockArgs
{ recipientWallet :: Wallet -- Cartera del destinatario
, totalAda :: Ada -- Cantidad (Ada) a vincular al contrato
}
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON, ToSchema)
type SmartContractSchema =
BlockchainActions
.\/ Endpoint "lock" LockArgs
.\/ Endpoint "unlock" LockArgs
Para recoger las peticiones de los usuarios neceistamos declarar los "endpoints" correspondientes como parte del programa. El conjunto de todos los endpoints se denomina "schema". Lo construiremos usando el tipo Endpoint y el operador .\/
para combinarlos.
Previamente hemos definido los par谩metros necesarios para los endpoints, que son la direcci贸n de la cartera destinataria y el cantidad de Ada a vincular con el contrato.
Se han desarrollado dos acciones para interactuar con el contrato lock y unlock.
lock :: Contract SmartContractSchema T.Text LockArgs
lock = endpoint @"lock"
unlock :: Contract SmartContractSchema T.Text LockArgs
unlock = endpoint @"unlock"
Endpoint recibe como argumento el nombre como tipo Haskell usando el operando @
. En general el algoritmo de unificaci贸n de Haskell es lo suficientemente robusto para los tipos de argumentos pasados a una funci贸n. Pero hay algunos casos, como este, en los que es necesario indicarle el tipo de dato.
mkSmartContractData :: LockArgs -> SmartContractData
mkSmartContractData LockArgs{recipientWallet, totalAda} =
let convert :: Wallet -> PubKeyHash
convert = pubKeyHash . walletPubKey
in
SmartContractData
{ recipient = convert recipientWallet
, amount = totalAda
}
Para realizar correctamente la transferencia de un tipo wallet a otro, es necesario un objeto pubKeyHash. Es necesario convertir el valor del wallet a su Hash de clave p煤blica.
lockFunds :: SmartContractData -> Contract SmartContractSchema T.Text ()
lockFunds s@SmartContractData{amount} = do
logInfo $ "Locking " <> show amount -- Muestra la cantidad de Ada vinculada al contrato por el registro
let tx = Constraints.mustPayToTheScript s (Ada.toValue amount)
void $ submitTxConstraints smartContractInstance tx
Con esta funci贸n vinculamos la cantidad obtenida del SmartContractData del usuario que inici贸 el contrato.
unlockFunds :: SmartContractData -> Contract SmartContractSchema T.Text ()
unlockFunds SmartContractData{recipient, amount} = do
let contractAddress = (Ledger.scriptAddress (Scripts.validatorScript smartContractInstance))
utxos <- utxoAt contractAddress
let tx =
collectFromScript utxos ()
<> Constraints.mustPayToPubKey recipient (Ada.toValue $ amount)
void $ submitTxConstraintsSpending smartContractInstance utxos tx
Mediante esta funci贸n el destinatario puede recibir el dinero que se ha depositado en el contrato. Gracias a la restricci贸n mustPayToPubKey
con destinatario recipient
y la cantidad de capital amount
.
El script de validaci贸n verificar谩 que el destinatario haya solicitado la cantidad exacta vinculada al contrato por el remitente.
endpoints :: Contract SmartContractSchema T.Text ()
endpoints = (lock >>= lockFunds . mkSmartContractData) `select` (unlock >>= unlockFunds . mkSmartContractData)
mkSchemaDefinitions ''SmartContractSchema
$(mkKnownCurrencies [])
Estas 煤ltimas lineas definen nuestra aplicaci贸n para que se pueda ejecutar en Plutus Playground. La funci贸n select ofrece dos ramificaciones para las carteras. Por un lado permite transferir el capital al contrato ejecutando la funci贸n lockFunds
y por otra, recibir el capital del contrato con la funci贸n unlockFunds
. Asegura que una cartera solo pueda interactuar con una parte del contrato (enviar o recibir) y sea otra la que complemente el contrato.