Skip to content

Instantly share code, notes, and snippets.

@matiwinnetou
Last active January 19, 2023 13:52
Show Gist options
  • Save matiwinnetou/cae2685b7e88db4e310245f84b5e802c to your computer and use it in GitHub Desktop.
Save matiwinnetou/cae2685b7e88db4e310245f84b5e802c to your computer and use it in GitHub Desktop.
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