Smart Contract Interactions
It's important to understand that on the Cardano blockchain, you don't directly interact with "smart contracts", atleast not in the traditional sense.
-
Instead, you work with validators. These validators are responsible for verifying the actions taken in a given transaction, rather than executing or calling any actions themselves.
-
A validator checks whether the transaction meets its requirements, and if it does, the transaction is processed successfully. If the requirements are not met, the transaction fails (is not allowed to execute).
Instantiate validator
Lucid Evolution consumes compiled validators. On-chain scripts can be written in PlutusTx
, Aiken
, Plutarch
, and many other languages available in the Cardano ecosystem. We then derive the address from the compiled script with Lucid Evolution.
const spend_val: SpendingValidator = {
type: "PlutusV2",
script: "59099a590997010000...", // from plutus.json of the compiled contract code
};
Working with Datums and Redeemers
Datums and redeemers are crucial for interacting with validators
const datum = Data.to(new Constr(0, [publicKeyHash]));
const redeemer = Data.to(new Constr(0, [fromText("Hello, World!")]));
You can also define datum schemas for type safety
const DatumSchema = Data.Object({
owner: Data.Bytes(),
});
type DatumType = Data.Static<typeof DatumSchema>;
const DatumType = DatumSchema as unknown as DatumType;
Lock funds at a plutus script address
const tx = await lucid
.newTx()
.pay.ToAddressWithData(
contractAddress,
{ kind: "inline", value: datum },
{ lovelace: 10_000_000n }
)
.complete();
Redeem from a plutus script address
const allUTxOs = await lucid.utxosAt(contractAddress);
const ownerUTxO = allUtxos.find((utxo) => {
if (utxo.datum) {
const datum = Data.from(utxo.datum, DatumType);
return datum.owner === publicKeyHash;
}
});
const tx = await lucid
.newTx()
.collectFrom([ownerUTxO], redeemer)
.attach.SpendingValidator(spend_val) // spend_val was defined earlier
.addSigner(address)
.complete();
Redeemer Builder
The Redeemer Indexing design pattern (opens in a new tab) leverages the deterministic script evaluation property of the Cardano ledger to achieve substantial performance gains in onchain code.
In scenarios where the protocol necessitates spending from the script back to a specific output—such as returning funds from the script to the same script, directing them to another script, or transferring assets to a wallet, it is imperative to ensure that each script input is uniquely associated with an output.
This preventive measure is essential for mitigating the risk of Double Satisfaction Attack (opens in a new tab).
You can use a redeemer containing one-to-one correlation between script input UTxOs and output UTxOs. This is provided via ordered lists of input/output indices of inputs/ouputs present in the Script Context.
For e.g.
Inputs : [scriptInputA, scriptInputB, randomInput1, scriptInputC, randomInput2, randomInput3] // random inputs are not the concerned script inputs
Outputs : [outputA, outputB, outputC, randomOuput1, randomOutput2, randomOutput3]
InputIdxs : [0, 1, 3]
OutputIdxs : [0, 1, 2]
where type Redeemer = List<(inputIdx, outputIdx)>
Luckily lucid-evolution (opens in a new tab)
provides a high-level interface that abstracts all the complexity away and makes writing offchain code for this design pattern extremely simple
by using the RedeemerBuilder
to help building your redeemer.
const scriptInputs: UTxO[] = [
{ txHash: "a", outputIndex: 1, address: "scriptAddress", assets: { lovelace: 10_000000n } },
{ txHash: "b", outputIndex: 2, address: "scriptAddress", assets: { lovelace: 20_000000n } },
{ txHash: "d", outputIndex: 4, address: "scriptAddress", assets: { lovelace: 40_000000n } },
];
const randomInputs: UTxO[] = [
{ txHash: "c", outputIndex: 3, address: "randomAddress", assets: { lovelace: 30_000000n } },
{ txHash: "e", outputIndex: 5, address: "randomAddress", assets: { lovelace: 50_000000n } },
{ txHash: "f", outputIndex: 6, address: "randomAddress", assets: { lovelace: 60_000000n } },
];
const redeemer: RedeemerBuilder = {
kind: "selected",
makeRedeemer: (inputIdxs: bigint[]) => {
const tupleList = inputIdxs.map((inputIdx, i) => [inputIdx, BigInt(i)]); // List<(inputIdx, outputIdx)>
return Data.to(tupleList);
},
inputs: scriptInputs,
};
const tx = await lucid
.newTx()
.collectFrom([...scriptInputs, ...randomInputs], redeemer)
.attach.SpendingValidator(script)
.pay.ToContract("scriptAddress", datum, { lovelace: 10_000000n })
.pay.ToContract("scriptAddress", datum, { lovelace: 20_000000n })
.pay.ToContract("scriptAddress", datum, { lovelace: 40_000000n })
.pay.ToAddress("randomAddress", { lovelace: 30_000000n })
.pay.ToAddress("randomAddress", { lovelace: 50_000000n })
.pay.ToAddress("randomAddress", { lovelace: 60_000000n })
.addSigner("randomAddress")
.complete();
Apply parameters
Some validators are parameterized, you can apply parameters dynamically.
Below example is for your validator when it expects 1 integer argument, eg. validator minting_policy(first_param: Int) { .. }
const mintingPolicy = {
type: "PlutusV3",
script: applyParamsToScript(
applyDoubleCborEncoding("5907945907910100..."),
[10n] // Parameters
),
};
Next example is for your validator when it expects 2 arguments, eg. a ByteArray and a Boolean.
For example, validator spending_validator(pkh: VerificationKeyHash, yes_no: Bool) { .. }
const pkh = paymentCredentialOf(address).hash;
const yes = new Constr(1, []);
const spendingValidator = {
type: "PlutusV3",
script: applyParamsToScript(
applyDoubleCborEncoding("5907945907910101..."),
[pkh, yes] // Parameters
),
};
The following example is for your validator when it expects a more complex type, for example:
type OutputReference {
transaction_id: ByteArray,
output_index: Int,
}
validator your_validator(o_ref: OutputReference) { .. }
const oRef = new Constr(0, [String(utxo.txHash), BigInt(utxo.outputIndex)]);
const yourValidator = {
type: "PlutusV3",
script: applyParamsToScript(
applyDoubleCborEncoding("5907945907910102..."),
[oRef] // Parameters
),
};
Note that this OutputReference format is Aiken's, for Plutus format,
data TxOutRef = TxOutRef
{ txOutRefId :: TxId
, txOutRefIdx :: Integer
}
where TxId itself is another type like newtype TxId = TxId { getTxId :: PlutusTx.BuiltinByteString }
,
this would translate to the following Aiken code:
type TransactionId {
tx_id: ByteArray
}
type OutputReference {
transaction_id: TransactionId,
output_index: Int,
}
validator your_validator(o_ref: OutputReference) { .. }
In this case, you can provide the parameter for your validator like:
const txID = new Constr(0, [String(utxo.txHash)]);
const txIdx = BigInt(utxo.outputIndex)
const oRef = new Constr(0, [txID, txIdx]);
const yourValidator = {
type: "PlutusV3",
script: applyParamsToScript(
applyDoubleCborEncoding("5907945907910103..."),
[oRef] // Parameters
),
};
Plutus script purposes
Like native scripts, Plutus scripts can be used not only for checking the spending conditions of UTxOs but also for verifying conditions related to minting, delegations, and withdrawals
.collectFrom(utxos, redeemer)
.mintAssets(assets, redeemer)
.delegateTo(stakeAddress, poolId, redeemer)
.deRegisterStake(stakeAddress, redeemer)
.withdraw(stakeAddress, rewardAmount, redeemer)
Multi validator interactions
You can run and execute multiple validators in a single transaction with Lucid Evolution. The only limitation is the execution units limit
const tx = await lucid
.newTx()
.collectFrom([scriptUtxoA, scriptUtxoB])
.collectFrom([scriptUtxoC])
.collectFrom([scriptUtxoD])
.mintAssets({ [plutusPolicyId]: 10n })
.attach.SpendingValidator(spendingScript1)
.attach.SpendingValidator(spendingScript2)
.attach.MintingPolicy(mintingPolicy)
.complete();
Read UTxOs and plutus scripts
Lucid Evolution allows you to conveniently read/reference UTxOs. If a Plutus script is already stored in the UTxO, there's no need to attach the same script explicitly in the transaction, resulting in cost savings.
const tx = await lucid
.newTx()
.readFrom([scriptUtxo])
.complete();
Remember that when interacting with Plutus scripts, you need to provide appropriate datums and redeemers. Always ensure your transaction meets the validator's requirements to avoid failures.