@@ -2,6 +2,7 @@ use aiken/bytearray
22use aiken/ cbor
33use aiken/ dict
44use aiken/ hash.{blake2b_256}
5+ use aiken/ interval.{Interval , IntervalBound , Finite , PositiveInfinity }
56use aiken/ list
67use aiken/ math/ rational
78use aiken/ option
@@ -16,13 +17,14 @@ use aiken/transaction.{
1617 NoDatum ,
1718 OutputReference ,
1819 TransactionId ,
19- Transaction
20+ Transaction ,
21+ ValidityRange
2022}
2123use aiken/ transaction/ credential.{Address , Credential , ScriptCredential , VerificationKeyCredential }
2224use aiken/ transaction/ value
2325
2426use optim/ types.{IdMintRedeemer , KeyHash , Id , MintId , AssetClass , ScriptHash }
25- use optim/ utils.{find_id_input, get_output_datum, mint_own_id}
27+ use optim/ utils.{burn_own_id, find_id_input, get_output_datum, mint_own_id}
2628use optim/ types/ oada.{StakingAmoDatum }
2729
2830pub type WrappedRedeemer < data> {
@@ -51,6 +53,27 @@ type Cip68Datum<extra> {
5153 extra: extra
5254}
5355
56+ validator (_tag: Int ) {
57+ fn always_mint (_r: Data , _ctx: Data ) {
58+ True
59+ }
60+
61+ fn always_spend (_d: Data , _r: Data , _ctx: Data ) {
62+ True
63+ }
64+ }
65+
66+ const one_day = 86_400_000 // 60 * 60 * 24
67+ // actually just gives an arbitrary time within the current day, taken from the interval
68+ pub fn validity_range_to_day (validity_range: ValidityRange ) -> Int {
69+ expect Finite (start_slot) = validity_range.lower_bound.bound_type
70+ expect Finite (end_slot) = validity_range.upper_bound.bound_type
71+ let start_day = start_slot / one_day
72+ let end_day = end_slot / one_day
73+ expect start_day == end_day
74+ end_slot
75+ }
76+
5477validator (donation_target: KeyHash , staking_amo: Id , batch_stake: ScriptHash ) {
5578 fn mint (redeemer: IdMintRedeemer , ctx: ScriptContext ) {
5679 let ScriptContext (tx_info, purpose) = ctx
@@ -96,6 +119,10 @@ validator(donation_target: KeyHash, staking_amo: Id, batch_stake: ScriptHash) {
96119 let otoken_yield =
97120 rational.sub (current_exchange, initial_exchange) |> rational.mul (sotoken)
98121 expect Some (donation_ratio) = rational.new (datum.donation_ratio.1st, datum.donation_ratio.2nd)
122+ let to_donate_base =
123+ otoken_yield
124+ |> rational.mul (donation_ratio)
125+ |> rational.truncate
99126 expect Some (to_donate) =
100127 otoken_yield
101128 |> rational.mul (donation_ratio)
@@ -116,10 +143,16 @@ validator(donation_target: KeyHash, staking_amo: Id, batch_stake: ScriptHash) {
116143 }
117144
118145 and {
119- (to_donate == redeemer.3rd)?,
120146 list.has (tx_info.extra_signatories, datum.owner)?,
121- validate_unstake_output (donation_target, to_donate, donation_unstake_output),
122- validate_unstake_output (datum.owner, rational.truncate (sotoken) - to_donate, change_unstake_output)
147+ burn_own_id (purpose, tx_info.inputs, tx_info.mint),
148+ or {
149+ to_donate_base <= 0 ,
150+ and {
151+ (to_donate_base == redeemer.3rd)?,
152+ validate_unstake_output (donation_target, to_donate, donation_unstake_output),
153+ validate_unstake_output (datum.owner, rational.truncate (sotoken) - to_donate, change_unstake_output)
154+ }
155+ }
123156 }
124157 }
125158}
@@ -157,21 +190,27 @@ validator(
157190 fn (output) { value.quantity_of (output.value, policy_id, reference_token_name) == 1 }
158191 )
159192
160- expect reference_token_datum: Cip68Datum < Int > = get_output_datum (reference_token_output, tx_info.datums)
193+ expect reference_token_datum: Cip68Datum < Void > = get_output_datum (reference_token_output, tx_info.datums)
161194
195+ let timestamp: Data = validity_range_to_day (tx_info.validity_range)
196+
197+ let donated_data: Data = donated
162198 let expected_reference_datum = Cip68Datum {
163199 metadata: [
164200 Pair ("description" , nft_description),
201+ Pair ("donated" , donated_data),
165202 Pair ("image" , nft_image_url),
166203 Pair ("name" , nft_name),
204+ Pair ("time" , timestamp)
167205 ],
168206 version: 1 ,
169- extra: donated
207+ extra: Void
170208 }
171209
172210 let reference_token_locked = reference_token_output.address.payment_credential == reference_holder
173211
174212 and {
213+ (donated > 0 )?,
175214 (own_tokens == [Pair (reference_token_name, 1 ), Pair (user_token_name, 1 )])?,
176215 (reference_token_datum == expected_reference_datum)?,
177216 reference_token_locked?
@@ -254,6 +293,13 @@ fn test_donation(
254293 let unburned_sotoken = locked_sotoken * 999 / 1000
255294 // doing this calculation inline just to avoid duplicating the validator code
256295 // as a sanity check
296+ let donated_base =
297+ unburned_sotoken
298+ * (sotoken_backing_1 - sotoken_backing_0)
299+ * donation_datum.donation_ratio.1st
300+ / donation_datum.donation_ratio.2nd
301+ / sotoken_amount
302+ let donated_base_data: Data = donated_base
257303 let donated_sotoken =
258304 unburned_sotoken
259305 * (sotoken_backing_1 - sotoken_backing_0)
@@ -283,7 +329,12 @@ fn test_donation(
283329 let nft_description: Data = "Proof of your donation"
284330 let nft_image_url: Data = "https://example.com/image.jpg"
285331
286- let donate_redeemer_wrapped: Data = WrappedRedeemer ((0 , 1 , donated_sotoken))
332+ let donate_redeemer_wrapped: Data = WrappedRedeemer ((0 , 1 , donated_base))
333+
334+ // arbitrary day in 2025
335+ let start = 365 * one_day * 55 + one_day * 200 + one_day / 3
336+ let end = start + 60 * 60
337+ let end_data: Data = end
287338
288339 let donate_transaction = Transaction {
289340 .. transaction.placeholder (),
@@ -319,18 +370,32 @@ fn test_donation(
319370 datum: InlineDatum (Cip68Datum {
320371 metadata: [
321372 Pair ("description" , nft_description),
373+ Pair ("donated" , donated_base_data),
322374 Pair ("image" , nft_image_url),
323- Pair ("name" , nft_name)
375+ Pair ("name" , nft_name),
376+ Pair ("time" , end_data)
324377 ],
325378 version: 1 ,
326- extra: donated_sotoken ,
379+ extra: Void ,
327380 }),
328381 reference_script: None ,
329382 }
330383 ] |> fn (outputs) { if !include_nft { list.take (outputs, 2 ) } else { outputs } },
331- mint: if !include_nft { value.zero () } else { minted_reference_nft |> value.merge (minted_user_nft) } |> value.to_minted_value,
384+ mint: if !include_nft { value.zero () } else { minted_reference_nft |> value.merge (minted_user_nft) }
385+ |> value.merge (value.negate (minted_id))
386+ |> value.to_minted_value,
332387 extra_signatories: [donation_datum.owner],
333- redeemers: [Pair (Spend (donation_output_ref), donate_redeemer_wrapped)]
388+ redeemers: [Pair (Spend (donation_output_ref), donate_redeemer_wrapped)],
389+ validity_range: Interval {
390+ lower_bound: IntervalBound {
391+ bound_type: Finite (start),
392+ is_inclusive: True ,
393+ },
394+ upper_bound: IntervalBound {
395+ bound_type: Finite (end),
396+ is_inclusive: True ,
397+ }
398+ }
334399 } |> transform_donate_transaction
335400
336401 expect actual_donation_datum: DonationDatum =
@@ -385,10 +450,10 @@ fn test_donation(
385450 }
386451}
387452
388- fn transform_reference_nft_datum (transform: fn (Cip68Datum < Int > ) -> Cip68Datum < Int > ) -> fn (Transaction ) -> Transaction {
453+ fn transform_reference_nft_datum (transform: fn (Cip68Datum < Void > ) -> Cip68Datum < Void > ) -> fn (Transaction ) -> Transaction {
389454 fn (t: Transaction ) {
390455 expect Some (reference_nft_output) = list.at (t.outputs, 2 )
391- expect reference_nft_datum: Cip68Datum < Int > = {
456+ expect reference_nft_datum: Cip68Datum < Void > = {
392457 expect InlineDatum (datum) = reference_nft_output.datum
393458 datum
394459 }
@@ -425,10 +490,69 @@ fn transform_batch_stake_datum(output_index: Int, transform: fn(BatchStakeDatum)
425490 }
426491}
427492
428- test test_donate () {
493+ fn test_cancel (
494+ transform_mint_transaction: fn (Transaction ) -> Transaction ,
495+ transform_cancel_transaction: fn (Transaction ) -> Transaction ,
496+ ) {
497+ test_donation (
498+ transform_mint_transaction,
499+ fn (t) {
500+ expect Some (donation_input) = list.at (t.inputs, 0 )
501+ expect donation_datum: DonationDatum = {
502+ expect InlineDatum (data) = donation_input.output.datum
503+ data
504+ }
505+
506+ expect Some (staking_amo_input) = list.at (t.reference_inputs, 0 )
507+ expect staking_amo_datum: StakingAmoDatum = {
508+ expect InlineDatum (data) = staking_amo_input.output.datum
509+ data
510+ }
511+
512+ let donation_redeemer: Data = WrappedRedeemer ((0 , 0 , 0 ))
513+
514+ Transaction {
515+ .. t,
516+ reference_inputs: [
517+ Input {
518+ .. staking_amo_input,
519+ output: Output {
520+ .. staking_amo_input.output,
521+ datum: InlineDatum (StakingAmoDatum {
522+ .. staking_amo_datum,
523+ sotoken_backing: donation_datum.initial_exchange.1st,
524+ sotoken_amount: donation_datum.initial_exchange.2nd
525+ })
526+ }
527+ }
528+ ],
529+ outputs: [
530+ Output {
531+ address: Address { payment_credential: VerificationKeyCredential (donation_datum.owner), stake_credential: None },
532+ value: donation_input.output.value,
533+ datum: NoDatum ,
534+ reference_script: None
535+ }
536+ ],
537+ redeemers: [Pair (Spend (donation_input.output_reference), donation_redeemer)],
538+ } |> transform_cancel_transaction
539+ },
540+ False
541+ )
542+ }
543+
544+ fn transform_validity_range (transform: fn (ValidityRange ) -> ValidityRange ) -> fn (Transaction ) -> Transaction {
545+ fn (t: Transaction ) { Transaction { .. t, validity_range: transform (t.validity_range) } }
546+ }
547+
548+ test test_donate_succeeds () {
429549 test_donation (fn (t) {t}, fn (t) {t}, True )
430550}
431551
552+ test test_cancel_succeeds () {
553+ test_cancel (fn (t) {t}, fn (t) {t})
554+ }
555+
432556// negative tests below heavily depend on knowledge of the structure and
433557// assumptions of `test_donation`
434558!test test_donate_missing_donation_output_fails () {
@@ -437,14 +561,20 @@ test test_donate() {
437561
438562!test test_donate_incorrect_donation_report_fails () {
439563 let donate_redeemer = (0 , 1 , 9999999999 )
564+ let donate_amount_data: Data = donate_redeemer.3rd
440565 let donate_redeemer_wrapped: Data = WrappedRedeemer (donate_redeemer)
441566 test_donation (fn (t) {t}, fn (t) {
442567 expect [donation_input] = t.inputs
443568 Transaction {
444569 .. t,
445570 redeemers: [Pair (Spend (donation_input.output_reference), donate_redeemer_wrapped)]
446571 } |> transform_reference_nft_datum (fn (datum) {
447- Cip68Datum { .. datum, extra: 9999999999 }
572+ Cip68Datum {
573+ .. datum,
574+ metadata: pairs.map (datum.metadata, fn (key, value: Data ) {
575+ if key == "donated" { donate_amount_data } else { value }
576+ })
577+ }
448578 })
449579 }, True )
450580}
@@ -478,6 +608,52 @@ test test_donate() {
478608 )
479609}
480610
611+ !test test_donate_bad_validity_range_fails () {
612+ or {
613+ test_donation (
614+ fn (t) {t},
615+ transform_validity_range (fn (validity_range: ValidityRange ) {
616+ expect Finite (range_end) = validity_range.upper_bound.bound_type
617+ Interval {
618+ .. validity_range,
619+ upper_bound: IntervalBound {
620+ .. validity_range.upper_bound,
621+ bound_type: Finite (range_end + one_day)
622+ }
623+ }
624+ }),
625+ True
626+ ),
627+ test_donation (
628+ fn (t) {t},
629+ transform_validity_range (fn (validity_range: ValidityRange ) {
630+ expect Finite (range_start) = validity_range.lower_bound.bound_type
631+ Interval {
632+ .. validity_range,
633+ lower_bound: IntervalBound {
634+ .. validity_range.lower_bound,
635+ bound_type: Finite (range_start - one_day)
636+ }
637+ }
638+ }),
639+ True
640+ ),
641+ test_donation (
642+ fn (t) {t},
643+ transform_validity_range (fn (validity_range: ValidityRange ) {
644+ Interval {
645+ .. validity_range,
646+ upper_bound: IntervalBound {
647+ .. validity_range.upper_bound,
648+ bound_type: PositiveInfinity
649+ }
650+ }
651+ }),
652+ True
653+ ),
654+ }
655+ }
656+
481657!test test_donate_missing_change_output_fails () {
482658 test_donation (
483659 fn (t) {t},
@@ -529,9 +705,15 @@ test test_donate() {
529705!test test_donate_bad_reference_nft_datum_fails () {
530706 or {
531707 test_donation (
532- fn (t) {t },
708+ fn (t) { t },
533709 transform_reference_nft_datum (fn (datum) {
534- Cip68Datum { .. datum, extra: 100000000 }
710+ Cip68Datum {
711+ .. datum,
712+ metadata: pairs.map (datum.metadata, fn (key, value: Data ) {
713+ let donated: Data = 100000000
714+ if key == "donated" { donated } else { value }
715+ })
716+ }
535717 }),
536718 True
537719 ),
@@ -586,3 +768,12 @@ test test_donate() {
586768 True // double satisfaction will succeed if we don't mint the NFT, but the donator has nothing to gain from doing this
587769 )
588770}
771+
772+ validator (token: AssetClass ) {
773+ fn token_unlock (_datum: Data , _redeemer: Data , ctx: ScriptContext ) {
774+ list.any (
775+ ctx.transaction.inputs,
776+ fn (input) { value.quantity_of (input.output.value, token.policy_id, token.asset_name) > 0 }
777+ )
778+ }
779+ }
0 commit comments