Last active
January 19, 2023 13:52
-
-
Save matiwinnetou/cae2685b7e88db4e310245f84b5e802c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use aiken/hash.{Blake2b_224, Hash} | |
use aiken/interval.{Finite, Interval, IntervalBound, PositiveInfinity} | |
use aiken/list | |
use aiken/option | |
use aiken/transaction.{ | |
Input, Output, OutputReference, ScriptContext, ScriptPurpose, Spend, | |
ValidityRange, | |
} | |
use aiken/transaction/credential.{ | |
Address, Inline, PublicKeyCredential, Script, ScriptCredential, | |
VerificationKey, | |
} | |
use aiken/transaction/value.{Value} | |
type VerificationKeyHash = | |
Hash<Blake2b_224, VerificationKey> | |
type POSIXTime = | |
Int | |
type Lovelace = | |
Int | |
type ValidatorHash = | |
Hash<Blake2b_224, Script> | |
type BidDatum { | |
bidder: VerificationKeyHash, | |
bid: Lovelace, | |
} | |
type Datum { | |
// there will always be seller identified by their wallet pub key hash | |
seller: VerificationKeyHash, | |
// seller sets min amount to bid | |
min_bid: Lovelace, | |
// seller sets deadline after which no more bids are accepted and an action can be closed | |
deadline: POSIXTime, | |
// the asset that is being auctioned (NFT + ADA lovelace) | |
for_sale: Value, | |
// initially highest_bid will be None, which means there are no bids yet | |
highest_bid: Option<BidDatum>, | |
} | |
type Redeemer { | |
Close | |
Bid { bidder: VerificationKeyHash, bid: Lovelace } | |
} | |
fn spend(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool { | |
english_auction( | |
datum, | |
redeemer, | |
ctx.transaction.validity_range, | |
ctx.purpose, | |
ctx.transaction.inputs, | |
ctx.transaction.outputs, | |
) | |
} | |
fn english_auction( | |
datum: Datum, | |
redeemer: Redeemer, | |
validity_range: ValidityRange, | |
script_purpose: ScriptPurpose, | |
trx_inputs: List<Input>, | |
trx_outputs: List<Output>, | |
) -> Bool { | |
assert Spend(output_reference) = script_purpose | |
assert Some(validator_hash) = | |
our_validator_script_address_hash(trx_inputs, output_reference) | |
when redeemer is { | |
Bid(bidder, bid) -> | |
bid_action( | |
validator_hash: validator_hash, | |
validity_range: validity_range, | |
datum: datum, | |
bid_datum: BidDatum { bid, bidder }, | |
outputs: trx_outputs, | |
) | |
Close -> | |
close_action( | |
datum: datum, | |
validity_range: validity_range, | |
outputs: trx_outputs, | |
) | |
} | |
} | |
fn bid_action( | |
validator_hash: ValidatorHash, | |
validity_range: ValidityRange, | |
datum: Datum, | |
bid_datum: BidDatum, | |
outputs: List<Output>, | |
) -> Bool { | |
// lets see if min bid threshold is met or not | |
let min_bid_threshold_met = bid_datum.bid > datum.min_bid | |
// if bid is high enough it may still be lower than current highest bidder | |
let higher_than_highest_current_bidder = | |
is_bid_higher_than_current_highest_bidder( | |
bid: bid_datum, | |
current_highest_bid: datum.highest_bid, | |
) | |
// can we still bid or deadline passed and action should not be allowed | |
let deadline_not_passed = must_start_before(validity_range, datum.deadline) | |
let bid_preconditions_met = | |
min_bid_threshold_met && higher_than_highest_current_bidder && deadline_not_passed | |
when bid_preconditions_met is { | |
True -> { | |
let datum_with_new_bidder: Datum = | |
Datum { ..datum, highest_bid: Some(bid_datum) } | |
let has_new_bid_have_token_and_new_bid = | |
has_new_bid_have_token_and_new_bid( | |
validator_hash: validator_hash, | |
datum_with_new_bidder: datum_with_new_bidder, | |
outputs: outputs, | |
) | |
let is_old_bidder_repaid = | |
is_old_bidder_repaid( | |
current_highest_bid: datum.highest_bid, | |
outputs: outputs, | |
) | |
has_new_bid_have_token_and_new_bid && is_old_bidder_repaid | |
} | |
// we do not approve bid action when bid pre-condition checks fail of course we return false, bid action is invalid | |
False -> False | |
} | |
} | |
fn close_action( | |
datum: Datum, | |
validity_range: ValidityRange, | |
outputs: List<Output>, | |
) -> Bool { | |
// if bidding deadline passed or it is still possible to place bids | |
let deadline_passed: Bool = must_start_after(validity_range, datum.deadline) | |
when deadline_passed is { | |
True -> { | |
let is_highest_bid_requirement_met = | |
is_highest_bid_requirement_met( | |
min_bid: datum.min_bid, | |
current_highest_bid: datum.highest_bid, | |
) | |
if is_highest_bid_requirement_met { | |
close_action_winner_gets_token( | |
datum: datum, | |
validity_range: validity_range, | |
outputs: outputs, | |
) | |
} else { | |
is_for_sale_returned_to_the_seller(datum, outputs) | |
} | |
} | |
// do not allow closing of auction if deadline has not passed | |
False -> False | |
} | |
} | |
fn close_action_winner_gets_token( | |
datum: Datum, | |
validity_range: ValidityRange, | |
outputs: List<Output>, | |
) -> Bool { | |
let has_winner_got_auction_token = winner_gets_auctioned_token(datum, outputs) | |
let has_seller_got_highest_bid = | |
seller_receives_highest_bid( | |
seller: datum.seller, | |
current_highest_bid: datum.highest_bid, | |
outputs: outputs, | |
) | |
has_winner_got_auction_token && has_seller_got_highest_bid | |
} | |
// we need to check that seller received the highest bid | |
fn seller_receives_highest_bid( | |
seller: VerificationKeyHash, | |
current_highest_bid: Option<BidDatum>, | |
outputs: List<Output>, | |
) -> Bool { | |
when current_highest_bid is { | |
Some(chb) -> | |
outputs | |
|> any_output_matches( | |
fn(payment_hash, output_value, _output_datum) { | |
payment_hash == seller && output_value == value.from_lovelace(chb.bid) | |
}) | |
None -> False | |
} | |
} | |
// this function will verify if old bidder got repaid | |
fn is_old_bidder_repaid( | |
current_highest_bid: Option<BidDatum>, | |
outputs: List<Output>, | |
) -> Bool { | |
when current_highest_bid is { | |
Some(chb) -> | |
outputs | |
|> any_output_matches( | |
fn(payment_hash, output_value, _output_datum) { | |
payment_hash == chb.bidder && output_value == value.from_lovelace( | |
chb.bid, | |
) | |
}) | |
None -> False | |
} | |
} | |
// on bid action - new highest bidder should have new token including bid amount (lovelaces) in one of the UTxO outputs | |
fn has_new_bid_have_token_and_new_bid( | |
validator_hash: ValidatorHash, | |
datum_with_new_bidder: Datum, | |
outputs: List<Output>, | |
) -> Bool { | |
// we need to make sure that new highest_bid is present in the new datum | |
when datum_with_new_bidder.highest_bid is { | |
Some(hb) -> { | |
let highest_bid = hb.bid | |
let datum_with_new_bidder_data: Data = datum_with_new_bidder | |
any_output_matches( | |
outputs, | |
fn(payment_hash, output_value, output_datum) { | |
// we need to verify if new bid also contains datum and if datum data matches | |
// it needs to have exactly the same values as defined by new_expected_datum requirements | |
let new_bid_contains_datum = | |
output_datum == datum_with_new_bidder_data | |
new_bid_contains_datum && payment_hash == validator_hash && output_value == value.add( | |
datum_with_new_bidder.for_sale, | |
value.from_lovelace(highest_bid), | |
) | |
}, | |
) | |
} | |
None -> False | |
} | |
} | |
// we need to check that after auction has finished if winner received auctioned token / NFT | |
fn winner_gets_auctioned_token(datum: Datum, outputs: List<Output>) -> Bool { | |
outputs | |
|> any_output_matches( | |
fn(payment_hash, value, _datum) { | |
payment_hash == datum.seller && value == value.without_lovelace( | |
datum.for_sale, | |
) | |
}) | |
} | |
// while closing auction we need to check if off-chain code returned the item to the seller | |
fn is_for_sale_returned_to_the_seller( | |
datum: Datum, | |
outputs: List<Output>, | |
) -> Bool { | |
outputs | |
|> any_output_matches( | |
fn(payment_hash, output_value, _output_datum) { | |
payment_hash == datum.seller && output_value == datum.for_sale | |
}) | |
} | |
fn our_validator_script_address_hash( | |
inputs: List<Input>, | |
output_reference: OutputReference, | |
) -> Option<ValidatorHash> { | |
inputs | |
|> list.find(fn(input) { input.output_reference == output_reference }) | |
|> option.map(fn(v) { v.output }) | |
|> option.map(fn(v) { v.address }) | |
|> option.map(fn(v) { v.payment_credential }) | |
|> option.map( | |
fn(v) { | |
when v is { | |
ScriptCredential(hash) -> Some(hash) | |
_ -> None | |
} | |
}) | |
|> option.flatten() | |
} | |
// we need to check if placed bid is higher than already placed bets | |
fn is_bid_higher_than_current_highest_bidder( | |
bid: BidDatum, | |
current_highest_bid: Option<BidDatum>, | |
) -> Bool { | |
when current_highest_bid is { | |
// we have a previous highest bid | |
Some(chb) -> bid.bid > chb.bid | |
None -> True | |
} | |
} | |
// we need to check if placed bid is higher than already placed bets | |
// this is sanity check | |
fn is_highest_bid_requirement_met( | |
min_bid: Lovelace, | |
current_highest_bid: Option<BidDatum>, | |
) -> Bool { | |
when current_highest_bid is { | |
// sanity check, min_bid has to be higher than current highest bid | |
Some(chb) -> chb.bid > min_bid | |
None -> True | |
} | |
} | |
fn must_start_before(range: ValidityRange, lower_bound: POSIXTime) -> Bool { | |
when range.lower_bound.bound_type is { | |
Finite(now) -> now < lower_bound | |
_ -> False | |
} | |
} | |
fn must_start_after(range: ValidityRange, lower_bound: POSIXTime) -> Bool { | |
when range.lower_bound.bound_type is { | |
Finite(now) -> now > lower_bound | |
_ -> False | |
} | |
} | |
test must_start_after_test_works() { | |
let now: Int = 2 | |
let validity_range = | |
Interval { | |
lower_bound: IntervalBound(Finite(now), True), | |
upper_bound: IntervalBound(PositiveInfinity, True), | |
} | |
let lower_bound = 1 | |
must_start_after(validity_range, lower_bound) | |
} | |
test must_start_after_test_fails() { | |
let now: Int = 1 | |
let validity_range = | |
Interval { | |
lower_bound: IntervalBound(Finite(now), True), | |
upper_bound: IntervalBound(PositiveInfinity, True), | |
} | |
let lower_bound = 2 | |
must_start_after(validity_range, lower_bound) == False | |
} | |
test must_start_before_test_works() { | |
let now: Int = 1 | |
let validity_range = | |
Interval { | |
lower_bound: IntervalBound(Finite(now), True), | |
upper_bound: IntervalBound(PositiveInfinity, True), | |
} | |
let lower_bound = 2 | |
must_start_before(validity_range, lower_bound) | |
} | |
test must_start_before_test_fails() { | |
let now: Int = 2 | |
let validity_range = | |
Interval { | |
lower_bound: IntervalBound(Finite(now), True), | |
upper_bound: IntervalBound(PositiveInfinity, True), | |
} | |
let lower_bound = 1 | |
must_start_before(validity_range, lower_bound) == False | |
} | |
fn any_output_matches( | |
outputs: List<Output>, | |
predicate: fn(ByteArray, Value, Data) -> Bool, | |
) -> Bool { | |
list.any( | |
outputs, | |
fn(output) { | |
let payment_hash = get_payment_addr_hash(output.address) | |
predicate(payment_hash, output.value, output.datum) | |
}, | |
) | |
} | |
fn mock_datum_data() -> Data { | |
let seller_hash_addr = #[1] | |
let policy_id1 = #[2] | |
let asset_name1 = #[3] | |
let nft = value.from_asset(policy_id1, asset_name1, 1) | |
let datum_as_data: Data = | |
Datum { | |
seller: seller_hash_addr, | |
min_bid: 1, | |
deadline: 1673966461, | |
for_sale: nft, | |
highest_bid: None, | |
} | |
datum_as_data | |
} | |
test any_output_matches_test() { | |
let b1: ByteArray = #[1] | |
let b2: ByteArray = #[2] | |
let v1 = value.from_lovelace(1) | |
let v2 = value.from_lovelace(2) | |
let d1 = mock_datum_data() | |
let a1 = | |
Address { | |
payment_credential: PublicKeyCredential(b1), | |
stake_credential: None, | |
} | |
let a2 = | |
Address { | |
payment_credential: PublicKeyCredential(b2), | |
stake_credential: None, | |
} | |
let outputs: List<Output> = [ | |
Output { address: a1, value: v1, datum: d1, reference_script: None }, | |
Output { address: a2, value: v2, datum: d1, reference_script: None }, | |
] | |
let check1 = | |
any_output_matches( | |
outputs, | |
fn(payment_hash, _output_value, _output_datum) { payment_hash == b1 }, | |
) | |
let check2 = | |
any_output_matches( | |
outputs, | |
fn(_payment_hash, output_value, _output_datum) { output_value == v1 }, | |
) | |
let check3 = | |
any_output_matches( | |
outputs, | |
fn(_payment_hash, _output_value, output_datum) { output_datum == d1 }, | |
) | |
check1 && check2 && check3 | |
} | |
fn get_payment_addr_hash(address: Address) -> VerificationKeyHash { | |
when address.payment_credential is { | |
PublicKeyCredential(hash) -> hash | |
ScriptCredential(hash) -> hash | |
} | |
} | |
test get_payment_addr_hash_public_key() { | |
let b1 = #[1] | |
let b2 = #[2] | |
let addr = | |
Address { | |
payment_credential: PublicKeyCredential(b1), | |
stake_credential: Some(Inline(ScriptCredential(b2))), | |
} | |
get_payment_addr_hash(addr) == b1 | |
} | |
test get_payment_addr_hash_script_key() { | |
let b2 = #[2] | |
let b3 = #[3] | |
let addr = | |
Address { | |
payment_credential: ScriptCredential(b3), | |
stake_credential: Some(Inline(ScriptCredential(b2))), | |
} | |
get_payment_addr_hash(addr) == b3 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment