Another Shared Wallet Script
The previous example used the POSIXTime
type as the script parameter. For the following example, we will look at custom-defined parameter types as there is some extra work we need to do in that case. We will transform the SharedWallet script from earlier to be parameterised instead of relying on the UTxO datum. That means that either of the trusted parties can spend any UTxO sitting at the script address, regardless of what the datum associated is.
Writing the validator
Firstly, we create the same type as before, this time calling it sharedParam
:
data SharedWalletParam = SharedWalletParam {
wallet1 :: Plutus.PubKeyHash,
wallet2 :: Plutus.PubKeyHash
}
As before, we need to make our parameter type an instance of ToData
/FromData
via PlutusTx.unstableMakeIsData
:
PlutusTx.unstableMakeIsData ''SharedWalletParam
In addition, when defining custom parameter types, we need to make them an instance of the Lift
class. This allows the type to be lifted into so-called Plutus IR (intermediate representation), which is then handled further to Plutus Core, but essentially, we just need this to allow the compiler to compile our type to Plutus Core. This is done with the PlutusTx.makeLift
function that automatically derives an instance for us:
PlutusTx.makeLift ''SharedWalletParam
Note that PubKeyHash
type is an instance of both ToData
/FromData
and Lift
, but our defined type SharedParam
which contains two PubKeyHash
fields is not.
The rest is pretty much the same as in the typed validator version. We need to create a ValidatorTypes
instance, but since we are using a parameter that will bake our sharedParam
inside the script, we do not need datum and redeemer:
data SharedWalletParamValidator
instance Scripts.ValidatorTypes SharedWalletParamValidator
-- default types for datum and redeemer are ()
Our mkValidator
function still for valid signatures in the transaction context, except this time the PubKeyHash
es are deconstructed from the SharedParam
argument:
{-# INLINEABLE mkValidator #-}
mkValidator :: SharedWalletParam -> () -> () -> Plutus.ScriptContext -> Bool
mkValidator swp () () ctx = checkSignature1 || checkSignature2
where
info :: Plutus.TxInfo
info = Plutus.scriptContextTxInfo ctx
signature1 :: PubKeyHash
signature1 = wallet1 swp
signature2 :: PubKeyHash
signature2 = wallet2 swp
checkSignature1 :: Bool
checkSignature1 = traceIfFalse "signature 1 missing" $ txSignedBy info signature1
checkSignature2 :: Bool
checkSignature2 = traceIfFalse "signature 2 missing" $ txSignedBy info signature2
As with the parameterised deadline script example, we use the PSU.V2.mkTypedValidatorParam
to define a function that accepts the parameter to compile the validator.
typedValidator :: SharedWalletParam -> PSU.V2.TypedValidator SharedWalletParamValidator
typedValidator =
PSU.V2.mkTypedValidatorParam @SharedWalletParamValidator
$$(PlutusTx.compile [||mkValidator||])
$$(PlutusTx.compile [||wrap||])
where
wrap = PSU.mkUntypedValidator
validator :: SharedWalletParam -> Plutus.Validator
validator = PSU.V2.validatorScript . typedValidator
script :: SharedWalletParam -> Plutus.Script
script = Plutus.unValidatorScript . validator
Besides the DataKind
extension we enabled when working with typed validators, for parameterised validators we also need to enable ScopedTypeVariables
and MultiParamTypeClasses
.
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
Finally, we define the functions to serialise and write the script file:
sharedWalletParamShortBs :: SharedWalletParam -> SBS.ShortByteString
sharedWalletParamShortBs swp = SBS.toShort . LBS.toStrict $ serialise $ script swp
scriptSerialised :: SharedWalletParam -> PlutusScript PlutusScriptV2
scriptSerialised swp = PlutusScriptSerialised $ sharedWalletParamShortBs swp
writeSerialisedScript :: SharedWalletParam -> IO (Either (FileError ()) ())
writeSerialisedScript swp = writeFileTextEnvelope "compiled/SharedWalletParam.plutus" Nothing $ scriptSerialised swp
In order to compile this validator, we need to provide it with a parameter of type SharedWalletParam
. Again, we can export this type and its constructor by adding it to the module interface.
module SharedWalletParam
(
scriptSerialised,
writeSerialisedScript,
SharedWalletParam (..)
)
where
...
Now, when we load the module in a REPL, we can construct a valid SharedWalletParam
type. Let's do that by specifying the two public key hashes from our addresses as we did with the typed version of the shared wallet validator. When we have our parameter for the script, we can pass it to the writeSerialisedScript
function.
Prelude> :l SharedWalletParam
[1 of 1] Compiling SharedWalletParam ( src/SharedWalletParam.hs, /home/plutus/hpm-plutus/hpm-validators/dist-newstyle/build/x86_64-linux/ghc-8.10.7/hpm-validators-0.1.0.0/build/SharedWalletParam.o )
Ok, one module loaded.
Prelude SharedWalletParam> :set -XOverloadedStrings
Prelude SharedWalletParam> myParam = SharedWalletParam "a5d318dadfb52eeffb260ae097f846aea0ca78e6cc4fe406d4ceedc0" "1b1e5895b03302b248e8c459817bab49471c4013a0806ac52cb73f9b"
Prelude SharedWalletParam> writeSerialisedScript myParam
Right ()
Testing the validator
As always, we start off by creating the script address in testnet/SharedWalletParam/create-script-address.sh
.
#!/usr/bin/env bash
NWMAGIC=2 # preview testnet
# Build script address
cardano-cli address build \
--payment-script-file ../../compiled/SharedWalletParam.plutus \
--testnet-magic $NWMAGIC \
--out-file SharedWalletParam.addr
echo "Script address: $(cat SharedWalletParam.addr)"
./create-script-address.sh
Script address: addr_test1wqjvw5a437sarsrexzezx04cl4rtxsd895cx4s3ncn7qg6gevx55f
Next, let's get an overview of the UTxOs with check-utxos.sh
.
#!/usr/bin/env bash
NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket
funds_normal1=$(cardano-cli query utxo \
--address $(cat ../address/01.addr) \
--testnet-magic $NWMAGIC)
funds_normal2=$(cardano-cli query utxo \
--address $(cat ../address/02.addr) \
--testnet-magic $NWMAGIC)
funds_script=$(cardano-cli query utxo \
--address $(cat SharedWalletParam.addr) \
--testnet-magic $NWMAGIC)
echo "Normal address1:"
echo "${funds_normal1}"
echo ""
echo "Normal address2:"
echo "${funds_normal2}"
echo ""
echo "Script address:"
echo "${funds_script}"
./check-utxos.sh
Normal address1:
TxHash TxIx Amount
--------------------------------------------------------------------------------------
5dc5111e257f8e68b0978c9619e57bbb12d365c0ec45d879115866bb674156ae 0 1826915 lovelace + TxOutDatumNone
6924903343947231af6c56a5d2d25b3256a513dee77e7966b6f8a47b09913188 2 9844804665 lovelace + TxOutDatumNone
ede24e9e40ca82830c75d827b5c3b090132c1afaebd3a4256655fb5d2382474a 0 9649776 lovelace + TxOutDatumNone
ee346be463426509daec07aba24a8905c5f55965daebb39f842a49191d83f9e1 0 1829006 lovelace + TxOutDatumNone
Normal address2:
TxHash TxIx Amount
--------------------------------------------------------------------------------------
59590fab00fb430d205151c59ca7e00af38e9945d778abdae6897f368aa39591 0 19682109 lovelace + TxOutDatumNone
667f81c9a89946d83f5975d9d97534df42be85a5a5aa1161b7af0ecb3d6592d0 0 19673177 lovelace + TxOutDatumNone
Script address:
TxHash TxIx Amount
--------------------------------------------------------------------------------------
We can now send some funds to the script. Remember, we don't care about the datum as the script was parameterised at compilation so it knows about our public key hashes. However, we always have to attach a datum to any script UTxO or it will be unspendable, so we can attach our unit.json
in the transaction constructed by the send-funds-to-script.sh
script.
#!/usr/bin/env bash
NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket
cardano-cli transaction build \
--testnet-magic $NWMAGIC \
--change-address $(cat ../address/01.addr) \
--tx-in 6924903343947231af6c56a5d2d25b3256a513dee77e7966b6f8a47b09913188#2 \
--tx-out $(cat SharedWalletParam.addr)+20000000 \
--tx-out-datum-embed-file ../../compiled/assets/unit.json \
--out-file tx.body
cardano-cli transaction sign \
--tx-body-file tx.body \
--signing-key-file ../address/01.skey \
--testnet-magic $NWMAGIC \
--out-file tx.signed
cardano-cli transaction submit \
--testnet-magic $NWMAGIC \
--tx-file tx.signed
./send-funds-to-script.sh
Estimated transaction fee: Lovelace 169109
Transaction successfully submitted.
./check-utxos.sh
...
Script address:
TxHash TxIx Amount
--------------------------------------------------------------------------------------
abe233c49d4162d886a0e38e2ed03739cc9feb0b5f38eb54d8417eb9821f039b 0 20000000 lovelace + TxOutDatumHash ScriptDataInBabbageEra "923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec"
We see the UTxO at the script address so we can try spending it by simply signing the transaction with one of our private keys. Let's use the key from the 02.addr
and use one of its UTxOs for collateral as well in spend-script-funds.sh
.
#!/usr/bin/env bash
NWMAGIC=2 # preview testnet
export CARDANO_NODE_SOCKET_PATH=$CNODE_HOME/sockets/node0.socket
cardano-cli transaction build \
--testnet-magic $NWMAGIC \
--change-address $(cat ../address/02.addr) \
--tx-in abe233c49d4162d886a0e38e2ed03739cc9feb0b5f38eb54d8417eb9821f039b#0 \
--tx-in-script-file ../../compiled/SharedWalletParam.plutus \
--tx-in-datum-file ../../compiled/assets/unit.json \
--tx-in-redeemer-file ../../compiled/assets/unit.json \
--tx-in-collateral 667f81c9a89946d83f5975d9d97534df42be85a5a5aa1161b7af0ecb3d6592d0#0 \
--required-signer-hash 1b1e5895b03302b248e8c459817bab49471c4013a0806ac52cb73f9b \
--out-file tx.body
cardano-cli transaction sign \
--tx-body-file tx.body \
--signing-key-file ../address/02.skey \
--testnet-magic $NWMAGIC \
--out-file tx.signed
cardano-cli transaction submit \
--testnet-magic $NWMAGIC \
--tx-file tx.signed
And we can successfully spend the funds from the script using the 02.addr
signing key!
./spend-script-funds.sh
Estimated transaction fee: Lovelace 315499
Transaction successfully submitted.
Last updated