diff --git a/examples/gift_card/validators/multi.ak b/examples/gift_card/validators/multi.ak new file mode 100644 index 00000000..8c4b7894 --- /dev/null +++ b/examples/gift_card/validators/multi.ak @@ -0,0 +1,168 @@ +use aiken/bytearray +use aiken/cbor +use aiken/dict +use aiken/hash.{blake2b_256} +use aiken/list +use aiken/transaction.{ + InlineDatum, Input, Output, ScriptContext, Spend, Transaction, +} as tx +use aiken/transaction/credential.{Address, PaymentCredential, ScriptCredential} +use aiken/transaction/value + +type Action { + Mint(Int) + Burn +} + +type SpendTokenName = + ByteArray + +validator(creator: ByteArray) { + fn redeem( + // Each spend input checks for a token name matching the datum being burned + datum: SpendTokenName, + _r: Data, + ctx: ScriptContext, + ) { + let ScriptContext { transaction, purpose } = + ctx + + let Transaction { inputs, mint, .. } = + transaction + + expect Spend(own_ref) = + purpose + + expect Some(own_input) = + list.find(inputs, fn(input) { input.output_reference == own_ref }) + + let Input { + output: Output { address: Address { payment_credential, .. }, .. }, + .. + } = + own_input + + expect ScriptCredential(own_validator_hash) = + payment_credential + + value.quantity_of(mint, own_validator_hash, datum) == -1 + } + + fn gift_card(rdmr: Action, ctx: ScriptContext) -> Bool { + // get values from transaction and purpose + let ScriptContext { transaction, purpose } = + ctx + + expect tx.Mint(policy_id) = + purpose + + let Transaction { inputs, mint, extra_signatories, outputs, .. } = + transaction + + let minted_assets = + mint + |> value.tokens(policy_id) + |> dict.to_list() + + when rdmr is { + Mint(total) -> { + expect [input, ..] = + inputs + // Base is created from serializing a utxo ref being spent. Thus this guarantees a unique base + let base = + cbor.serialise(input.output_reference) + // Create a list of expected token names + let expected_minted_token_names = + create_expected_minted_nfts(base, total, []) + // Check contract creator is a signer of this tx + let signature_check = + list.any(extra_signatories, fn(n) { creator == n }) + // Support multiple gift card creation by allowing a + // 'number of tokens minted' == 'outputs with datum being token name' + signature_check && check_mint_and_outputs( + minted_assets, + outputs, + expected_minted_token_names, + ScriptCredential(policy_id), + ) + } + Burn -> + list.all( + minted_assets, + fn(asset) { + let (_, amount) = + asset + amount == -1 + }, + ) + } + } +} + +fn insert(self: List, e: a, compare: fn(a, a) -> Ordering) -> List { + when self is { + [] -> + [e] + [x, ..xs] -> + if compare(e, x) == Less { + [e, ..self] + } else { + [x, ..insert(xs, e, compare)] + } + } +} + +// Check each minted token name is in the expected list, has quantity of 1, +// and has a corresponding ouput with datum containing token name. +// Otherwise fail +fn check_mint_and_outputs( + minted_assets: List<(ByteArray, Int)>, + outputs: List, + expected_assets: List, + validator_cred: PaymentCredential, +) -> Bool { + when minted_assets is { + [] -> + True + [(minted_asset_name, quantity), ..rest_assets] -> { + expect True = + list.any( + expected_assets, + fn(expected_asset) { expected_asset == minted_asset_name }, + ) + expect True = + list.any( + outputs, + fn(output) { + let Output { address, datum, .. } = + output + datum == InlineDatum(minted_asset_name) && address.payment_credential == validator_cred + }, + ) + quantity == 1 && check_mint_and_outputs( + rest_assets, + outputs, + expected_assets, + validator_cred, + ) + } + } +} + +fn create_expected_minted_nfts( + base: ByteArray, + counter: Int, + accum: List, +) -> List { + if counter == 0 { + accum + } else { + let token_name = + blake2b_256(bytearray.push(base, counter)) + + let accum = + [token_name, ..accum] + + create_expected_minted_nfts(base, counter - 1, accum) + } +}