From 3cfeb156c5826914a6b1a8a8205f7ecb8178040b Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Sun, 25 Feb 2024 11:35:00 -0800 Subject: [PATCH 01/30] starting it off here --- lib/aiken/transaction/goverance.ak | 0 lib/aiken/transactionV3.ak | 228 +++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 lib/aiken/transaction/goverance.ak create mode 100644 lib/aiken/transactionV3.ak diff --git a/lib/aiken/transaction/goverance.ak b/lib/aiken/transaction/goverance.ak new file mode 100644 index 0000000..e69de29 diff --git a/lib/aiken/transactionV3.ak b/lib/aiken/transactionV3.ak new file mode 100644 index 0000000..35b0367 --- /dev/null +++ b/lib/aiken/transactionV3.ak @@ -0,0 +1,228 @@ +use aiken/builtin +use aiken/dict.{Dict} +use aiken/hash.{Blake2b_224, Blake2b_256, Hash, blake2b_256} +use aiken/interval.{Interval} +use aiken/list +use aiken/option +use aiken/time.{PosixTime} +use aiken/transaction/certificate.{Certificate} +use aiken/transaction/credential.{ + Address, Script, ScriptCredential, StakeCredential, VerificationKey, + VerificationKeyCredential, +} +use aiken/transaction/value.{MintedValue, PolicyId, Value} + +/// A context given to a script by the Cardano ledger when being executed. +/// +/// The context contains information about the entire transaction that contains +/// the script. The transaction may also contain other scripts; to distinguish +/// between multiple scripts, the `ScriptContext` also contains a `purpose` +/// which indicates which script (or, for what purpose) of the transaction is +/// being executed. +pub type ScriptContextV3 { + transaction: Transaction, + purpose: ScriptPurposeV3, +} + +/// Characterizes the kind of script being executed. +pub type ScriptPurposeV3 { + /// For scripts executed as minting/burning policies, to insert + /// or remove assets from circulation. It's parameterized by the identifier + /// of the associated policy. + Mint(PolicyId) + /// For scripts that are used as payment credentials for addresses in + /// transaction outputs. They govern the rule by which the output they + /// reference can be spent. + Spend(OutputReference) + /// For scripts that validate reward withdrawals from a reward account. + /// + /// The argument identifies the target reward account. + WithdrawFrom(StakeCredential) + /// Needed when delegating to a pool using stake credentials defined as a + /// Plutus script. This purpose is also triggered when de-registering such + /// stake credentials. + /// + /// It embeds the certificate that's being validated. + Publish(Certificate) + /// + Vote(Int, Int) + // + Propose(Int) +} + +/// A Cardano `Transaction`, as seen by Plutus scripts. +/// +/// Note that this is a representation of a transaction, and not the 1:1 +/// translation of the transaction as seen by the ledger. In particular, +/// Plutus scripts can't see inputs locked by bootstrap addresses, outputs +/// to bootstrap addresses or just transaction metadata. +pub type Transaction { + inputs: List, + reference_inputs: List, + outputs: List, + fee: Value, + mint: MintedValue, + certificates: List, + withdrawals: Dict, + validity_range: ValidityRange, + extra_signatories: List>, + redeemers: Dict, + datums: Dict, Data>, + id: TransactionId, +} + +/// A placeholder / empty `Transaction` to serve as a base in a transaction +/// builder. This is particularly useful for constructing test transactions. +/// +/// Every field is empty or null, and we have in particular: +/// +/// ```aiken +/// use aiken/transaction +/// +/// transaction.placeholder().id == TransactionId { +/// hash: #"0000000000000000000000000000000000000000000000000000000000000000", +/// } +/// +/// transaction.placeholder().validity_range == interval.everything() +/// ``` +pub fn placeholder() -> Transaction { + Transaction { + inputs: [], + reference_inputs: [], + outputs: [], + fee: value.zero(), + mint: value.zero() |> value.to_minted_value(), + certificates: [], + withdrawals: dict.new(), + validity_range: interval.everything(), + extra_signatories: [], + redeemers: dict.new(), + datums: dict.new(), + id: TransactionId { + hash: #"0000000000000000000000000000000000000000000000000000000000000000", + }, + } +} + +/// An interval of POSIX time, measured in number milliseconds since 1970-01-01T00:00:00Z. +pub type ValidityRange = + Interval + +/// A unique transaction identifier, as the hash of a transaction body. Note that the transaction id +/// isn't a direct hash of the `Transaction` as visible on-chain. Rather, they correspond to hash +/// digests of transaction body as they are serialized on the network. +pub type TransactionId { + hash: Hash, +} + +/// An `Input` made of an output reference and, the resolved value associated with that output. +pub type Input { + output_reference: OutputReference, + output: Output, +} + +/// An `OutputReference` is a unique reference to an output on-chain. The `output_index` +/// corresponds to the position in the output list of the transaction (identified by its id) +/// that produced that output +pub type OutputReference { + transaction_id: TransactionId, + output_index: Int, +} + +/// A transaction `Output`, with an address, a value and optional datums and script references. +pub type Output { + address: Address, + value: Value, + datum: Datum, + reference_script: Option>, +} + +/// An output `Datum`. +pub type Datum { + NoDatum + /// A datum referenced by its hash digest. + DatumHash(Hash) + /// A datum completely inlined in the output. + InlineDatum(Data) +} + +/// A type-alias for Redeemers, passed to scripts for validation. The `Data` is +/// opaque because it is user-defined and it is the script's responsibility to +/// parse it into its expected form. +pub type Redeemer = + Data + +/// Find an input by its [`OutputReference`](#OutputReference). This is typically used in +/// combination with the `Spend` [`ScriptPurpose`](#ScriptPurpose) to find a script's own +/// input. +/// +/// ```aiken +/// validator { +/// fn(datum, redeemer, ctx: ScriptContext) { +/// expect Spend(my_output_reference) = +/// ctx.purpose +/// +/// expect Some(input) = +/// ctx.transaction.inputs +/// |> transaction.find_input(my_output_reference) +/// } +/// } +/// ``` +pub fn find_input( + inputs: List, + output_reference: OutputReference, +) -> Option { + inputs + |> list.find(fn(input) { input.output_reference == output_reference }) +} + +/// Find a [`Datum`](#Datum) by its hash, if present. The function looks first for +/// datums in the witness set, and then for inline datums if it doesn't find any in +/// witnesses. +pub fn find_datum( + outputs: List, + datums: Dict, Data>, + datum_hash: Hash, +) -> Option { + datums + |> dict.get(datum_hash) + |> option.or_try( + fn() { + outputs + |> list.filter_map( + fn(output) { + when output.datum is { + InlineDatum(data) -> + if + blake2b_256(builtin.serialise_data(data)) == datum_hash{ + + Some(data) + } else { + None + } + _ -> None + } + }, + ) + |> list.head + }, + ) +} + +/// Find all outputs that are paying into the given script hash, if any. This is useful for +/// contracts running over multiple transactions. +pub fn find_script_outputs( + outputs: List, + script_hash: Hash, +) -> List { + outputs + |> list.filter( + fn(output) { + when output.address.payment_credential is { + ScriptCredential(addr_script_hash) -> + script_hash == addr_script_hash + VerificationKeyCredential(_) -> False + } + }, + ) +} From cfcbc4a28b770cadc800391d4b4de9074f85d39a Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Sun, 25 Feb 2024 11:36:01 -0800 Subject: [PATCH 02/30] starting it off here --- .github/workflows/continuous-integration.yml | 64 -------------------- 1 file changed, 64 deletions(-) delete mode 100644 .github/workflows/continuous-integration.yml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml deleted file mode 100644 index 399db4d..0000000 --- a/.github/workflows/continuous-integration.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Continuous Integration - -on: - workflow_dispatch: - push: - branches: ["main"] - tags: ["*.*.*"] - pull_request: - branches: ["main"] - -env: - CARGO_TERM_COLOR: always - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: 📥 Checkout repository - uses: actions/checkout@v3 - - - name: 🧰 Setup Pages - uses: actions/configure-pages@v2 - - - name: 🧰 Install Aiken - uses: aiken-lang/setup-aiken@v1 - with: - version: v1.0.28-alpha - - - name: 📝 Run fmt - run: aiken fmt --check - - - name: 🔬 Run tests - run: aiken check - - - name: 📘 Generate documentation - shell: bash - working-directory: . - run: aiken docs -o docs - - - name: 📦 Upload artifact - uses: actions/upload-pages-artifact@v2 - with: - path: "docs/" - - deploy: - # if: ${{ startsWith(github.ref, 'refs/tags') }} - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: 🚀 Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v1 From 446282366ae6ff991814f98387ad76cfca9ea532 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 26 Feb 2024 15:45:26 -0800 Subject: [PATCH 03/30] making a branch for v3 not a file --- lib/aiken/transaction.ak | 106 ++++++++++++-- lib/aiken/transaction/goverance.ak | 0 lib/aiken/transactionV3.ak | 228 ----------------------------- 3 files changed, 91 insertions(+), 243 deletions(-) delete mode 100644 lib/aiken/transaction/goverance.ak delete mode 100644 lib/aiken/transactionV3.ak diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index eb1572e..f5e58f3 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -3,15 +3,82 @@ use aiken/dict.{Dict} use aiken/hash.{Blake2b_224, Blake2b_256, Hash, blake2b_256} use aiken/interval.{Interval} use aiken/list +use aiken/math/rational.{Rational} use aiken/option use aiken/time.{PosixTime} use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ - Address, Script, ScriptCredential, StakeCredential, VerificationKey, - VerificationKeyCredential, + Address, Credential, Script, ScriptCredential, StakeCredential, + VerificationKey, VerificationKeyCredential, } use aiken/transaction/value.{MintedValue, PolicyId, Value} +pub type GovernanceActionId { + gaid_tx_id: TransactionId, + gaid_tx_idx: Int, +} + +pub type ChangedParameters = + Data + +pub type ProtocolVersion { + major: Int, + minor: Int, +} + +pub type ColdCommitteeCredential { + member: Credential, +} + +pub type Constitution { + constitution_script: Option>, +} + +// CredentialDelegation { delegator: StakeCredential, delegatee: PoolId } + +pub type GovernanceAction { + ParameterChange( + Option, + ChangedParameters, + Option>, + ) + HardForkInitiation(Option, ProtocolVersion) + TreasuryWithdrawals( + Dict, + Option>, + ) + NoConfidence(Option) + UpdateCommittee( + Option, + List, + Dict, + Rational, + ) + NewConstitution(Option, Constitution) + InfoAction +} + +pub type ProposalProcedure { + deposit: Lovelace, + return_addr: Credential, + governance_action: GovernanceAction, +} + +pub type Voter { + CommitteeVoter(Certificate) + DRepVoter(Certificate) + StakePoolVoter(Hash) +} + +pub type Vote { + VoteNo + VoteYes + Abstain +} + +pub type Lovelace = + Int + /// A context given to a script by the Cardano ledger when being executed. /// /// The context contains information about the entire transaction that contains @@ -43,7 +110,11 @@ pub type ScriptPurpose { /// stake credentials. /// /// It embeds the certificate that's being validated. - Publish(Certificate) + Publish(Int, Certificate) + /// + Voting(Voter) + /// + Proposing(Int, ProposalProcedure) } /// A Cardano `Transaction`, as seen by Plutus scripts. @@ -56,15 +127,19 @@ pub type Transaction { inputs: List, reference_inputs: List, outputs: List, - fee: Value, - mint: MintedValue, + fee: Lovelace, + mint: Value, certificates: List, - withdrawals: Pairs, + withdrawals: Pairs, validity_range: ValidityRange, extra_signatories: List>, redeemers: Pairs, datums: Dict, Data>, id: TransactionId, + votes: Dict>, + proposal_procedures: List, + current_treasury_amount: Option, + treasury_donation: Option, } /// A placeholder / empty `Transaction` to serve as a base in a transaction @@ -86,17 +161,19 @@ pub fn placeholder() -> Transaction { inputs: [], reference_inputs: [], outputs: [], - fee: value.zero(), - mint: value.zero() |> value.to_minted_value(), + fee: 0, + mint: value.zero(), certificates: [], withdrawals: [], validity_range: interval.everything(), extra_signatories: [], redeemers: [], datums: dict.new(), - id: TransactionId { - hash: #"0000000000000000000000000000000000000000000000000000000000000000", - }, + id: #"0000000000000000000000000000000000000000000000000000000000000000", + votes: dict.new(), + proposal_procedures: [], + current_treasury_amount: None, + treasury_donation: None, } } @@ -107,9 +184,8 @@ pub type ValidityRange = /// A unique transaction identifier, as the hash of a transaction body. Note that the transaction id /// isn't a direct hash of the `Transaction` as visible on-chain. Rather, they correspond to hash /// digests of transaction body as they are serialized on the network. -pub type TransactionId { - hash: Hash, -} +pub type TransactionId = + Hash /// An `Input` made of an output reference and, the resolved value associated with that output. pub type Input { @@ -191,7 +267,7 @@ pub fn find_datum( InlineDatum(data) -> if blake2b_256(builtin.serialise_data(data)) == datum_hash{ - + Some(data) } else { None diff --git a/lib/aiken/transaction/goverance.ak b/lib/aiken/transaction/goverance.ak deleted file mode 100644 index e69de29..0000000 diff --git a/lib/aiken/transactionV3.ak b/lib/aiken/transactionV3.ak deleted file mode 100644 index 35b0367..0000000 --- a/lib/aiken/transactionV3.ak +++ /dev/null @@ -1,228 +0,0 @@ -use aiken/builtin -use aiken/dict.{Dict} -use aiken/hash.{Blake2b_224, Blake2b_256, Hash, blake2b_256} -use aiken/interval.{Interval} -use aiken/list -use aiken/option -use aiken/time.{PosixTime} -use aiken/transaction/certificate.{Certificate} -use aiken/transaction/credential.{ - Address, Script, ScriptCredential, StakeCredential, VerificationKey, - VerificationKeyCredential, -} -use aiken/transaction/value.{MintedValue, PolicyId, Value} - -/// A context given to a script by the Cardano ledger when being executed. -/// -/// The context contains information about the entire transaction that contains -/// the script. The transaction may also contain other scripts; to distinguish -/// between multiple scripts, the `ScriptContext` also contains a `purpose` -/// which indicates which script (or, for what purpose) of the transaction is -/// being executed. -pub type ScriptContextV3 { - transaction: Transaction, - purpose: ScriptPurposeV3, -} - -/// Characterizes the kind of script being executed. -pub type ScriptPurposeV3 { - /// For scripts executed as minting/burning policies, to insert - /// or remove assets from circulation. It's parameterized by the identifier - /// of the associated policy. - Mint(PolicyId) - /// For scripts that are used as payment credentials for addresses in - /// transaction outputs. They govern the rule by which the output they - /// reference can be spent. - Spend(OutputReference) - /// For scripts that validate reward withdrawals from a reward account. - /// - /// The argument identifies the target reward account. - WithdrawFrom(StakeCredential) - /// Needed when delegating to a pool using stake credentials defined as a - /// Plutus script. This purpose is also triggered when de-registering such - /// stake credentials. - /// - /// It embeds the certificate that's being validated. - Publish(Certificate) - /// - Vote(Int, Int) - // - Propose(Int) -} - -/// A Cardano `Transaction`, as seen by Plutus scripts. -/// -/// Note that this is a representation of a transaction, and not the 1:1 -/// translation of the transaction as seen by the ledger. In particular, -/// Plutus scripts can't see inputs locked by bootstrap addresses, outputs -/// to bootstrap addresses or just transaction metadata. -pub type Transaction { - inputs: List, - reference_inputs: List, - outputs: List, - fee: Value, - mint: MintedValue, - certificates: List, - withdrawals: Dict, - validity_range: ValidityRange, - extra_signatories: List>, - redeemers: Dict, - datums: Dict, Data>, - id: TransactionId, -} - -/// A placeholder / empty `Transaction` to serve as a base in a transaction -/// builder. This is particularly useful for constructing test transactions. -/// -/// Every field is empty or null, and we have in particular: -/// -/// ```aiken -/// use aiken/transaction -/// -/// transaction.placeholder().id == TransactionId { -/// hash: #"0000000000000000000000000000000000000000000000000000000000000000", -/// } -/// -/// transaction.placeholder().validity_range == interval.everything() -/// ``` -pub fn placeholder() -> Transaction { - Transaction { - inputs: [], - reference_inputs: [], - outputs: [], - fee: value.zero(), - mint: value.zero() |> value.to_minted_value(), - certificates: [], - withdrawals: dict.new(), - validity_range: interval.everything(), - extra_signatories: [], - redeemers: dict.new(), - datums: dict.new(), - id: TransactionId { - hash: #"0000000000000000000000000000000000000000000000000000000000000000", - }, - } -} - -/// An interval of POSIX time, measured in number milliseconds since 1970-01-01T00:00:00Z. -pub type ValidityRange = - Interval - -/// A unique transaction identifier, as the hash of a transaction body. Note that the transaction id -/// isn't a direct hash of the `Transaction` as visible on-chain. Rather, they correspond to hash -/// digests of transaction body as they are serialized on the network. -pub type TransactionId { - hash: Hash, -} - -/// An `Input` made of an output reference and, the resolved value associated with that output. -pub type Input { - output_reference: OutputReference, - output: Output, -} - -/// An `OutputReference` is a unique reference to an output on-chain. The `output_index` -/// corresponds to the position in the output list of the transaction (identified by its id) -/// that produced that output -pub type OutputReference { - transaction_id: TransactionId, - output_index: Int, -} - -/// A transaction `Output`, with an address, a value and optional datums and script references. -pub type Output { - address: Address, - value: Value, - datum: Datum, - reference_script: Option>, -} - -/// An output `Datum`. -pub type Datum { - NoDatum - /// A datum referenced by its hash digest. - DatumHash(Hash) - /// A datum completely inlined in the output. - InlineDatum(Data) -} - -/// A type-alias for Redeemers, passed to scripts for validation. The `Data` is -/// opaque because it is user-defined and it is the script's responsibility to -/// parse it into its expected form. -pub type Redeemer = - Data - -/// Find an input by its [`OutputReference`](#OutputReference). This is typically used in -/// combination with the `Spend` [`ScriptPurpose`](#ScriptPurpose) to find a script's own -/// input. -/// -/// ```aiken -/// validator { -/// fn(datum, redeemer, ctx: ScriptContext) { -/// expect Spend(my_output_reference) = -/// ctx.purpose -/// -/// expect Some(input) = -/// ctx.transaction.inputs -/// |> transaction.find_input(my_output_reference) -/// } -/// } -/// ``` -pub fn find_input( - inputs: List, - output_reference: OutputReference, -) -> Option { - inputs - |> list.find(fn(input) { input.output_reference == output_reference }) -} - -/// Find a [`Datum`](#Datum) by its hash, if present. The function looks first for -/// datums in the witness set, and then for inline datums if it doesn't find any in -/// witnesses. -pub fn find_datum( - outputs: List, - datums: Dict, Data>, - datum_hash: Hash, -) -> Option { - datums - |> dict.get(datum_hash) - |> option.or_try( - fn() { - outputs - |> list.filter_map( - fn(output) { - when output.datum is { - InlineDatum(data) -> - if - blake2b_256(builtin.serialise_data(data)) == datum_hash{ - - Some(data) - } else { - None - } - _ -> None - } - }, - ) - |> list.head - }, - ) -} - -/// Find all outputs that are paying into the given script hash, if any. This is useful for -/// contracts running over multiple transactions. -pub fn find_script_outputs( - outputs: List, - script_hash: Hash, -) -> List { - outputs - |> list.filter( - fn(output) { - when output.address.payment_credential is { - ScriptCredential(addr_script_hash) -> - script_hash == addr_script_hash - VerificationKeyCredential(_) -> False - } - }, - ) -} From bc0ba311f6058095c783fd87adb5b33c02f0f058 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 26 Feb 2024 15:48:54 -0800 Subject: [PATCH 04/30] doing a quick test downstream --- lib/aiken/transaction.ak | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index f5e58f3..dba4b44 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -11,7 +11,7 @@ use aiken/transaction/credential.{ Address, Credential, Script, ScriptCredential, StakeCredential, VerificationKey, VerificationKeyCredential, } -use aiken/transaction/value.{MintedValue, PolicyId, Value} +use aiken/transaction/value.{PolicyId, Value} pub type GovernanceActionId { gaid_tx_id: TransactionId, @@ -64,6 +64,10 @@ pub type ProposalProcedure { governance_action: GovernanceAction, } +pub type HotCommitteeCredential { + member: Credential, +} + pub type Voter { CommitteeVoter(Certificate) DRepVoter(Certificate) From f9efe34a3bd74308441dc57a07bd40efd024a0b3 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 26 Feb 2024 16:53:53 -0800 Subject: [PATCH 05/30] reviewing types --- lib/aiken/transaction.ak | 104 ++++++++++++++++++++++----- lib/aiken/transaction/certificate.ak | 4 ++ 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index dba4b44..fe827db 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -6,20 +6,66 @@ use aiken/list use aiken/math/rational.{Rational} use aiken/option use aiken/time.{PosixTime} -use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ Address, Credential, Script, ScriptCredential, StakeCredential, VerificationKey, VerificationKeyCredential, } use aiken/transaction/value.{PolicyId, Value} +pub type DRepCredential { + member: Credential, +} + +pub type DRep { + DRep(DRepCredential) + DRepAlwaysAbstain + DRepAlwaysNoConfidence +} + +pub type Delegatee { + DelegStake(PubKeyHash) + DelegVote(DRep) + DelegStakeVote(PubKeyHash, DRep) +} + +pub type Certificate { + // Register staking credential with an optional deposit amount + RegisterStaking(Credential, Option) + // Un-Register staking credential with an optional refund amount + UnRegisterStaking(Credential, Option) + // Delegate staking credential to a Delegatee + TxCertDelegStaking(Credential, Delegatee) + // Register and delegate staking credential to a Delegatee in one certificate. Noter that + // deposit is mandatory. + TxCertRegDeleg(Credential, Delegatee, Lovelace) + // Register a DRep with a deposit value. The optional anchor is omitted. + TxCertRegDRep(DRepCredential, Lovelace) + // Update a DRep. The optional anchor is omitted. + TxCertUpdateDRep(DRepCredential) + // UnRegister a DRep with mandatory refund value + TxCertUnRegDRep(DRepCredential, Lovelace) + // A digest of the PoolParams + TxCertPoolRegister( + // poolId + PubKeyHash, + // pool VFR + PubKeyHash, + ) + // The retirement certificate and the Epoch in which the retirement will take place + TxCertPoolRetire(PubKeyHash, Int) + // Authorize a Hot credential for a specific Committee member's cold credential + TxCertAuthHotCommittee(ColdCommitteeCredential, HotCommitteeCredential) + TxCertResignColdCommittee(ColdCommitteeCredential) +} + pub type GovernanceActionId { gaid_tx_id: TransactionId, gaid_tx_idx: Int, } -pub type ChangedParameters = - Data +pub type ChangedParameters { + new_parameters: Data, +} pub type ProtocolVersion { major: Int, @@ -31,30 +77,40 @@ pub type ColdCommitteeCredential { } pub type Constitution { - constitution_script: Option>, + constitution_script: Option, } -// CredentialDelegation { delegator: StakeCredential, delegatee: PoolId } - pub type GovernanceAction { + // ParameterChange( Option, ChangedParameters, - Option>, + // Hash of the constitution script + Option, ) + // proposal to update protocol version HardForkInitiation(Option, ProtocolVersion) + // TreasuryWithdrawals( Dict, - Option>, + // Hash of the constitution script + Option, ) + // NoConfidence(Option) + // UpdateCommittee( Option, + // Committee members to be removed List, + // Committee members to be added Dict, + // New quorum Rational, ) + // NewConstitution(Option, Constitution) + // InfoAction } @@ -69,9 +125,9 @@ pub type HotCommitteeCredential { } pub type Voter { - CommitteeVoter(Certificate) - DRepVoter(Certificate) - StakePoolVoter(Hash) + CommitteeVoter(HotCommitteeCredential) + DRepVoter(DRepCredential) + StakePoolVoter(PubKeyHash) } pub type Vote { @@ -80,9 +136,16 @@ pub type Vote { Abstain } +/// Lovelace is now a type wrapper for Int. pub type Lovelace = Int +pub type PubKeyHash = + Hash + +pub type ScriptHash = + Hash + /// A context given to a script by the Cardano ledger when being executed. /// /// The context contains information about the entire transaction that contains @@ -113,11 +176,16 @@ pub type ScriptPurpose { /// Plutus script. This purpose is also triggered when de-registering such /// stake credentials. /// - /// It embeds the certificate that's being validated. + /// The Int is a 0-based index of the given `TxCert` in `certificates`. Publish(Int, Certificate) + /// Voting for a type of voter using a governance action id to vote + /// yes / no / abstain inside a transaction. /// + /// The voter is who is doing the governance action. Voting(Voter) + /// Used to propose a governance action. /// + /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. Proposing(Int, ProposalProcedure) } @@ -132,14 +200,18 @@ pub type Transaction { reference_inputs: List, outputs: List, fee: Lovelace, + // ^ the fee is now lovelace mint: Value, + // ^ minted value is now back to a value certificates: List, - withdrawals: Pairs, + withdrawals: Pairs, validity_range: ValidityRange, - extra_signatories: List>, + extra_signatories: List, redeemers: Pairs, datums: Dict, Data>, id: TransactionId, + // ^ new tx id + // all new fields below for v3 votes: Dict>, proposal_procedures: List, current_treasury_amount: Option, @@ -210,7 +282,7 @@ pub type Output { address: Address, value: Value, datum: Datum, - reference_script: Option>, + reference_script: Option, } /// An output `Datum`. @@ -289,7 +361,7 @@ pub fn find_datum( /// contracts running over multiple transactions. pub fn find_script_outputs( outputs: List, - script_hash: Hash, + script_hash: ScriptHash, ) -> List { outputs |> list.filter( diff --git a/lib/aiken/transaction/certificate.ak b/lib/aiken/transaction/certificate.ak index a5d9c8e..390744c 100644 --- a/lib/aiken/transaction/certificate.ak +++ b/lib/aiken/transaction/certificate.ak @@ -1,6 +1,10 @@ use aiken/hash.{Blake2b_224, Hash} use aiken/transaction/credential.{PoolId, StakeCredential, VerificationKey} +/// Lovelace is now a type wrapper for Int. +pub type Lovelace = + Int + /// An on-chain certificate attesting of some operation. Publishing /// certificates / triggers different kind of rules; most of the time, /// they require signatures from / specific keys. From 78b1357547b66fc226775451646aabe0f13c8051 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 26 Feb 2024 17:11:22 -0800 Subject: [PATCH 06/30] split off the files --- lib/aiken/transaction.ak | 145 ++------------------------- lib/aiken/transaction/certificate.ak | 46 ++++++--- lib/aiken/transaction/credential.ak | 18 ++++ lib/aiken/transaction/governance.ak | 89 ++++++++++++++++ lib/aiken/transaction/value.ak | 4 + 5 files changed, 151 insertions(+), 151 deletions(-) create mode 100644 lib/aiken/transaction/governance.ak diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index fe827db..c6e263e 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -1,150 +1,19 @@ use aiken/builtin use aiken/dict.{Dict} -use aiken/hash.{Blake2b_224, Blake2b_256, Hash, blake2b_256} +use aiken/hash.{Blake2b_256, Hash, blake2b_256} use aiken/interval.{Interval} use aiken/list -use aiken/math/rational.{Rational} use aiken/option use aiken/time.{PosixTime} +use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ - Address, Credential, Script, ScriptCredential, StakeCredential, - VerificationKey, VerificationKeyCredential, + Address, Credential, PubKeyHash, ScriptCredential, ScriptHash, StakeCredential, + VerificationKeyCredential, } -use aiken/transaction/value.{PolicyId, Value} - -pub type DRepCredential { - member: Credential, -} - -pub type DRep { - DRep(DRepCredential) - DRepAlwaysAbstain - DRepAlwaysNoConfidence -} - -pub type Delegatee { - DelegStake(PubKeyHash) - DelegVote(DRep) - DelegStakeVote(PubKeyHash, DRep) -} - -pub type Certificate { - // Register staking credential with an optional deposit amount - RegisterStaking(Credential, Option) - // Un-Register staking credential with an optional refund amount - UnRegisterStaking(Credential, Option) - // Delegate staking credential to a Delegatee - TxCertDelegStaking(Credential, Delegatee) - // Register and delegate staking credential to a Delegatee in one certificate. Noter that - // deposit is mandatory. - TxCertRegDeleg(Credential, Delegatee, Lovelace) - // Register a DRep with a deposit value. The optional anchor is omitted. - TxCertRegDRep(DRepCredential, Lovelace) - // Update a DRep. The optional anchor is omitted. - TxCertUpdateDRep(DRepCredential) - // UnRegister a DRep with mandatory refund value - TxCertUnRegDRep(DRepCredential, Lovelace) - // A digest of the PoolParams - TxCertPoolRegister( - // poolId - PubKeyHash, - // pool VFR - PubKeyHash, - ) - // The retirement certificate and the Epoch in which the retirement will take place - TxCertPoolRetire(PubKeyHash, Int) - // Authorize a Hot credential for a specific Committee member's cold credential - TxCertAuthHotCommittee(ColdCommitteeCredential, HotCommitteeCredential) - TxCertResignColdCommittee(ColdCommitteeCredential) -} - -pub type GovernanceActionId { - gaid_tx_id: TransactionId, - gaid_tx_idx: Int, -} - -pub type ChangedParameters { - new_parameters: Data, -} - -pub type ProtocolVersion { - major: Int, - minor: Int, -} - -pub type ColdCommitteeCredential { - member: Credential, +use aiken/transaction/governance.{ + GovernanceActionId, ProposalProcedure, Vote, Voter, } - -pub type Constitution { - constitution_script: Option, -} - -pub type GovernanceAction { - // - ParameterChange( - Option, - ChangedParameters, - // Hash of the constitution script - Option, - ) - // proposal to update protocol version - HardForkInitiation(Option, ProtocolVersion) - // - TreasuryWithdrawals( - Dict, - // Hash of the constitution script - Option, - ) - // - NoConfidence(Option) - // - UpdateCommittee( - Option, - // Committee members to be removed - List, - // Committee members to be added - Dict, - // New quorum - Rational, - ) - // - NewConstitution(Option, Constitution) - // - InfoAction -} - -pub type ProposalProcedure { - deposit: Lovelace, - return_addr: Credential, - governance_action: GovernanceAction, -} - -pub type HotCommitteeCredential { - member: Credential, -} - -pub type Voter { - CommitteeVoter(HotCommitteeCredential) - DRepVoter(DRepCredential) - StakePoolVoter(PubKeyHash) -} - -pub type Vote { - VoteNo - VoteYes - Abstain -} - -/// Lovelace is now a type wrapper for Int. -pub type Lovelace = - Int - -pub type PubKeyHash = - Hash - -pub type ScriptHash = - Hash +use aiken/transaction/value.{Lovelace, PolicyId, Value} /// A context given to a script by the Cardano ledger when being executed. /// diff --git a/lib/aiken/transaction/certificate.ak b/lib/aiken/transaction/certificate.ak index 390744c..4eba715 100644 --- a/lib/aiken/transaction/certificate.ak +++ b/lib/aiken/transaction/certificate.ak @@ -1,19 +1,39 @@ -use aiken/hash.{Blake2b_224, Hash} -use aiken/transaction/credential.{PoolId, StakeCredential, VerificationKey} - -/// Lovelace is now a type wrapper for Int. -pub type Lovelace = - Int +use aiken/transaction/credential.{ + ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, + PubKeyHash, +} +use aiken/transaction/governance.{Delegatee} +use aiken/transaction/value.{Lovelace} /// An on-chain certificate attesting of some operation. Publishing /// certificates / triggers different kind of rules; most of the time, /// they require signatures from / specific keys. pub type Certificate { - CredentialRegistration { delegator: StakeCredential } - CredentialDeregistration { delegator: StakeCredential } - CredentialDelegation { delegator: StakeCredential, delegatee: PoolId } - PoolRegistration { pool_id: PoolId, vrf: Hash } - PoolDeregistration { pool_id: PoolId, epoch: Int } - Governance - TreasuryMovement + // Register staking credential with an optional deposit amount + RegisterStaking(Credential, Option) + // Un-Register staking credential with an optional refund amount + UnRegisterStaking(Credential, Option) + // Delegate staking credential to a Delegatee + DelegateStaking(Credential, Delegatee) + // Register and delegate staking credential to a Delegatee in one certificate. Noter that + // deposit is mandatory. + RegisterAndDelegate(Credential, Delegatee, Lovelace) + // Register a DRep with a deposit value. The optional anchor is omitted. + RegisterDRep(DRepCredential, Lovelace) + // Update a DRep. The optional anchor is omitted. + UpdateDRep(DRepCredential) + // UnRegister a DRep with mandatory refund value + UnRegisterDRep(DRepCredential, Lovelace) + // A digest of the PoolParams + PoolRegister( + // poolId + PubKeyHash, + // pool VFR + PubKeyHash, + ) + // The retirement certificate and the Epoch in which the retirement will take place + PoolRetire(PubKeyHash, Int) + // Authorize a Hot credential for a specific Committee member's cold credential + AuthorizeHotCommittee(ColdCommitteeCredential, HotCommitteeCredential) + ResignColdCommittee(ColdCommitteeCredential) } diff --git a/lib/aiken/transaction/credential.ak b/lib/aiken/transaction/credential.ak index fa6327d..4312dcd 100644 --- a/lib/aiken/transaction/credential.ak +++ b/lib/aiken/transaction/credential.ak @@ -110,3 +110,21 @@ pub type PaymentCredential = /// A unique stake pool identifier, as a hash of its owner verification key. pub type PoolId = Hash + +pub type PubKeyHash = + Hash + +pub type ScriptHash = + Hash + +pub type DRepCredential { + member: Credential, +} + +pub type ColdCommitteeCredential { + member: Credential, +} + +pub type HotCommitteeCredential { + member: Credential, +} diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak new file mode 100644 index 0000000..f778349 --- /dev/null +++ b/lib/aiken/transaction/governance.ak @@ -0,0 +1,89 @@ +use aiken/dict.{Dict} +use aiken/math/rational.{Rational} +use aiken/transaction/credential.{ + ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, + PubKeyHash, ScriptHash, +} +use aiken/transaction/value.{Lovelace} + +pub type DRep { + DRep(DRepCredential) + DRepAlwaysAbstain + DRepAlwaysNoConfidence +} + +pub type Delegatee { + DelegStake(PubKeyHash) + DelegVote(DRep) + DelegStakeVote(PubKeyHash, DRep) +} + +pub type Vote { + VoteNo + VoteYes + Abstain +} + +pub type GovernanceActionId { + gaid_tx_id: ByteArray, + gaid_tx_idx: Int, +} + +pub type ChangedParameters { + new_parameters: Data, +} + +pub type ProtocolVersion { + major: Int, + minor: Int, +} + +pub type Constitution { + constitution_script: Option, +} + +pub type GovernanceAction { + // + ParameterChange( + Option, + ChangedParameters, + // Hash of the constitution script + Option, + ) + // proposal to update protocol version + HardForkInitiation(Option, ProtocolVersion) + // + TreasuryWithdrawals( + Dict, + // Hash of the constitution script + Option, + ) + // + NoConfidence(Option) + // + UpdateCommittee( + Option, + // Committee members to be removed + List, + // Committee members to be added + Dict, + // New quorum + Rational, + ) + // + NewConstitution(Option, Constitution) + // + InfoAction +} + +pub type ProposalProcedure { + deposit: Lovelace, + return_addr: Credential, + governance_action: GovernanceAction, +} + +pub type Voter { + CommitteeVoter(HotCommitteeCredential) + DRepVoter(DRepCredential) + StakePoolVoter(PubKeyHash) +} diff --git a/lib/aiken/transaction/value.ak b/lib/aiken/transaction/value.ak index eac2203..7892c8e 100644 --- a/lib/aiken/transaction/value.ak +++ b/lib/aiken/transaction/value.ak @@ -4,6 +4,10 @@ use aiken/list use aiken/option use aiken/transaction/credential.{Script} +/// Lovelace is now a type wrapper for Int. +pub type Lovelace = + Int + /// A type-alias for a `PolicyId`. A `PolicyId` is always 28-byte long pub type PolicyId = Hash From d7e9aa103469e11597b45ca3f7e6ff163c6b532a Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 26 Feb 2024 17:27:00 -0800 Subject: [PATCH 07/30] general credential is used a lot --- lib/aiken/transaction.ak | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index c6e263e..9ced6eb 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -7,7 +7,7 @@ use aiken/option use aiken/time.{PosixTime} use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ - Address, Credential, PubKeyHash, ScriptCredential, ScriptHash, StakeCredential, + Address, Credential, PubKeyHash, ScriptCredential, ScriptHash, VerificationKeyCredential, } use aiken/transaction/governance.{ @@ -40,7 +40,7 @@ pub type ScriptPurpose { /// For scripts that validate reward withdrawals from a reward account. /// /// The argument identifies the target reward account. - WithdrawFrom(StakeCredential) + WithdrawFrom(Credential) /// Needed when delegating to a pool using stake credentials defined as a /// Plutus script. This purpose is also triggered when de-registering such /// stake credentials. From 4739f220b752e9aac2d43b42af414f71f080bb08 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Fri, 1 Mar 2024 11:05:52 -0800 Subject: [PATCH 08/30] begin code review --- .gitignore | 3 ++- lib/aiken/transaction.ak | 20 +++++++++++--------- lib/aiken/transaction/credential.ak | 5 ++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 3a3d38e..41e3c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ docs/ -.DS_Store \ No newline at end of file +.DS_Store +.github/workflows/ \ No newline at end of file diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 9ced6eb..35fac79 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -7,7 +7,7 @@ use aiken/option use aiken/time.{PosixTime} use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ - Address, Credential, PubKeyHash, ScriptCredential, ScriptHash, + Address, Credential, DatumHashDigest, PubKeyHash, ScriptCredential, ScriptHash, VerificationKeyCredential, } use aiken/transaction/governance.{ @@ -77,9 +77,8 @@ pub type Transaction { validity_range: ValidityRange, extra_signatories: List, redeemers: Pairs, - datums: Dict, Data>, + datums: Dict, id: TransactionId, - // ^ new tx id // all new fields below for v3 votes: Dict>, proposal_procedures: List, @@ -114,7 +113,9 @@ pub fn placeholder() -> Transaction { extra_signatories: [], redeemers: [], datums: dict.new(), - id: #"0000000000000000000000000000000000000000000000000000000000000000", + id: TransactionId { + hash: #"0000000000000000000000000000000000000000000000000000000000000000", + }, votes: dict.new(), proposal_procedures: [], current_treasury_amount: None, @@ -129,8 +130,9 @@ pub type ValidityRange = /// A unique transaction identifier, as the hash of a transaction body. Note that the transaction id /// isn't a direct hash of the `Transaction` as visible on-chain. Rather, they correspond to hash /// digests of transaction body as they are serialized on the network. -pub type TransactionId = - Hash +pub type TransactionId { + hash: Hash, +} /// An `Input` made of an output reference and, the resolved value associated with that output. pub type Input { @@ -158,7 +160,7 @@ pub type Output { pub type Datum { NoDatum /// A datum referenced by its hash digest. - DatumHash(Hash) + DatumHash(DatumHashDigest) /// A datum completely inlined in the output. InlineDatum(Data) } @@ -198,8 +200,8 @@ pub fn find_input( /// witnesses. pub fn find_datum( outputs: List, - datums: Dict, Data>, - datum_hash: Hash, + datums: Dict, + datum_hash: DatumHashDigest, ) -> Option { datums |> dict.get(datum_hash) diff --git a/lib/aiken/transaction/credential.ak b/lib/aiken/transaction/credential.ak index 4312dcd..0dcfe5a 100644 --- a/lib/aiken/transaction/credential.ak +++ b/lib/aiken/transaction/credential.ak @@ -1,5 +1,5 @@ use aiken/builtin -use aiken/hash.{Blake2b_224, Hash} +use aiken/hash.{Blake2b_224, Blake2b_256, Hash} /// A general structure for representing an on-chain `Credential`. /// @@ -117,6 +117,9 @@ pub type PubKeyHash = pub type ScriptHash = Hash +pub type DatumHashDigest = + Hash + pub type DRepCredential { member: Credential, } From 0bd5b1f501a41776e08938bbeea7f80557ba93dc Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:57:06 -0700 Subject: [PATCH 09/30] Update lib/aiken/transaction/governance.ak ChangedParameters -> ChangedProtocolParameters Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index f778349..d482b4e 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -46,7 +46,7 @@ pub type GovernanceAction { // ParameterChange( Option, - ChangedParameters, + ChangedProtocolParameters, // Hash of the constitution script Option, ) From a8d99710e09b393d7214e94d4237c90efc9cf6f3 Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:59:38 -0700 Subject: [PATCH 10/30] Update lib/aiken/transaction/governance.ak ParameterChange -> ProtocolParameterChange Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index d482b4e..5ed4218 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -44,7 +44,7 @@ pub type Constitution { pub type GovernanceAction { // - ParameterChange( + ProtocolParametersChange( Option, ChangedProtocolParameters, // Hash of the constitution script From b5816a2cbc90e0e2cf0d352c8ba3f793756ab1a0 Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:28:08 -0700 Subject: [PATCH 11/30] Update lib/aiken/transaction/governance.ak return_addr -> return_address Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 5ed4218..5a9bedb 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -78,7 +78,7 @@ pub type GovernanceAction { pub type ProposalProcedure { deposit: Lovelace, - return_addr: Credential, + return_address: Credential, governance_action: GovernanceAction, } From 9be059a8d5003e43c09382cbe46940286405a626 Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:29:44 -0700 Subject: [PATCH 12/30] Update lib/aiken/transaction/governance.ak constitution -> guardrails Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 5a9bedb..0df8151 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -39,7 +39,7 @@ pub type ProtocolVersion { } pub type Constitution { - constitution_script: Option, + guardrails_script: Option, } pub type GovernanceAction { From ebcd2d33dcde04d4e69e3fc6e06ff44c342b477d Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:33:23 -0700 Subject: [PATCH 13/30] Update lib/aiken/transaction/governance.ak Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 0df8151..2469b30 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -25,8 +25,8 @@ pub type Vote { } pub type GovernanceActionId { - gaid_tx_id: ByteArray, - gaid_tx_idx: Int, + transaction_id: ByteArray, + index: Int, } pub type ChangedParameters { From 41f6edde2c2696068ef7e7784f611a91670f794f Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:36:48 -0700 Subject: [PATCH 14/30] Update lib/aiken/transaction/governance.ak Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 2469b30..154c106 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -13,9 +13,9 @@ pub type DRep { } pub type Delegatee { - DelegStake(PubKeyHash) - DelegVote(DRep) - DelegStakeVote(PubKeyHash, DRep) + DelegateStake(PubKeyHash) + DelegateVote(DRep) + DelegateBoth(PubKeyHash, DRep) } pub type Vote { From d66c6bdfc4c535aa751bbd7f9cfd15511f0a943e Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:37:02 -0700 Subject: [PATCH 15/30] Update lib/aiken/transaction/governance.ak Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 154c106..81c6858 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -19,8 +19,8 @@ pub type Delegatee { } pub type Vote { - VoteNo - VoteYes + No + Yes Abstain } From c979ca20be75678f00dcd683e3667ebdf514bb69 Mon Sep 17 00:00:00 2001 From: Logical Mechanism <77127298+logicalmechanism@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:37:14 -0700 Subject: [PATCH 16/30] Update lib/aiken/transaction/governance.ak Co-authored-by: Matthias Benkort <5680256+KtorZ@users.noreply.github.com> --- lib/aiken/transaction/governance.ak | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 81c6858..4b9c942 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -7,9 +7,9 @@ use aiken/transaction/credential.{ use aiken/transaction/value.{Lovelace} pub type DRep { - DRep(DRepCredential) - DRepAlwaysAbstain - DRepAlwaysNoConfidence + FromCredential(DRepCredential) + AlwaysAbstain + AlwaysNoConfidence } pub type Delegatee { From b2705257a4eb68f37a2afdbc2b127513ed51f9ee Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 11 Mar 2024 12:52:10 -0700 Subject: [PATCH 17/30] Name changes and adding back in the yml workspace file --- .github/workflows/continuous-integration.yml | 64 ++++++++++++++++++++ .gitignore | 3 +- README.md | 4 +- lib/aiken/hash.ak | 2 +- lib/aiken/transaction.ak | 16 +++-- lib/aiken/transaction/certificate.ak | 10 +-- lib/aiken/transaction/credential.ak | 26 ++++---- lib/aiken/transaction/governance.ak | 10 +-- 8 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/continuous-integration.yml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..e6d2b5e --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,64 @@ +name: Continuous Integration + +on: + workflow_dispatch: + push: + branches: ["main"] + tags: ["*.*.*"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout repository + uses: actions/checkout@v3 + + - name: 🧰 Setup Pages + uses: actions/configure-pages@v2 + + - name: 🧰 Install Aiken + uses: aiken-lang/setup-aiken@v0.1.0 + with: + version: v1.0.18-alpha + + - name: 📝 Run fmt + run: aiken fmt --check + + - name: 🔬 Run tests + run: aiken check + + - name: 📘 Generate documentation + shell: bash + working-directory: . + run: aiken docs -o docs + + - name: 📦 Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: "docs/" + + deploy: + if: ${{ startsWith(github.ref, 'refs/tags') }} + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: 🚀 Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.gitignore b/.gitignore index 41e3c2c..3a3d38e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ build/ docs/ -.DS_Store -.github/workflows/ \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index a33bf3f..1db4a2e 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,10 @@ and aliases that make using Aiken a bliss. use aiken/hash.{Blake2b_224, Hash} use aiken/list use aiken/transaction.{ScriptContext} -use aiken/transaction/credential.{VerificationKey} +use aiken/transaction/credential.{PublicKey} pub type Datum { - owner: Hash, + owner: Hash, } pub type Redeemer { diff --git a/lib/aiken/hash.ak b/lib/aiken/hash.ak index 4f86027..13d1c4d 100644 --- a/lib/aiken/hash.ak +++ b/lib/aiken/hash.ak @@ -22,7 +22,7 @@ //// //// ```aiken //// pub type Credential { -//// VerificationKeyCredential(Hash) +//// VerificationKeyCredential(Hash) //// ScriptCredential(Hash) //// } //// ``` diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 35fac79..1c6b26c 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -7,8 +7,8 @@ use aiken/option use aiken/time.{PosixTime} use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ - Address, Credential, DatumHashDigest, PubKeyHash, ScriptCredential, ScriptHash, - VerificationKeyCredential, + Address, Credential, DatumHashDigest, PublicKeyHash, ScriptCredential, + ScriptHash, VerificationKeyCredential, } use aiken/transaction/governance.{ GovernanceActionId, ProposalProcedure, Vote, Voter, @@ -46,8 +46,16 @@ pub type ScriptPurpose { /// stake credentials. /// /// The Int is a 0-based index of the given `TxCert` in `certificates`. +<<<<<<< HEAD Publish(Int, Certificate) /// Voting for a type of voter using a governance action id to vote +||||||| parent of ee511e7 (Name changes and adding back in the yml workspace file) + Publish(Int, Certificate) + /// Voting for a type of voter using a governance action id to vote +======= + Publish { certificate_index: Int, certificate: Certificate } + /// Voting for a type of voter using a governance action id to vote +>>>>>>> ee511e7 (Name changes and adding back in the yml workspace file) /// yes / no / abstain inside a transaction. /// /// The voter is who is doing the governance action. @@ -55,7 +63,7 @@ pub type ScriptPurpose { /// Used to propose a governance action. /// /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. - Proposing(Int, ProposalProcedure) + Proposing { proposal_index: Int, proposal: ProposalProcedure } } /// A Cardano `Transaction`, as seen by Plutus scripts. @@ -75,7 +83,7 @@ pub type Transaction { certificates: List, withdrawals: Pairs, validity_range: ValidityRange, - extra_signatories: List, + extra_signatories: List, redeemers: Pairs, datums: Dict, id: TransactionId, diff --git a/lib/aiken/transaction/certificate.ak b/lib/aiken/transaction/certificate.ak index 4eba715..1e0ae27 100644 --- a/lib/aiken/transaction/certificate.ak +++ b/lib/aiken/transaction/certificate.ak @@ -1,6 +1,6 @@ use aiken/transaction/credential.{ ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, - PubKeyHash, + PoolId, PublicKeyHash, } use aiken/transaction/governance.{Delegatee} use aiken/transaction/value.{Lovelace} @@ -23,16 +23,16 @@ pub type Certificate { // Update a DRep. The optional anchor is omitted. UpdateDRep(DRepCredential) // UnRegister a DRep with mandatory refund value - UnRegisterDRep(DRepCredential, Lovelace) + UnregisterDRep(DRepCredential, Lovelace) // A digest of the PoolParams PoolRegister( // poolId - PubKeyHash, + PoolId, // pool VFR - PubKeyHash, + PublicKeyHash, ) // The retirement certificate and the Epoch in which the retirement will take place - PoolRetire(PubKeyHash, Int) + PoolRetire(PublicKeyHash, Int) // Authorize a Hot credential for a specific Committee member's cold credential AuthorizeHotCommittee(ColdCommitteeCredential, HotCommitteeCredential) ResignColdCommittee(ColdCommitteeCredential) diff --git a/lib/aiken/transaction/credential.ak b/lib/aiken/transaction/credential.ak index 0dcfe5a..3dab57e 100644 --- a/lib/aiken/transaction/credential.ak +++ b/lib/aiken/transaction/credential.ak @@ -6,7 +6,7 @@ use aiken/hash.{Blake2b_224, Blake2b_256, Hash} /// Credentials are always one of two kinds: a direct public/private key /// pair, or a script (native or Plutus). pub type Credential { - VerificationKeyCredential(Hash) + VerificationKeyCredential(Hash) ScriptCredential(Hash) } @@ -21,8 +21,8 @@ pub type Address { stake_credential: Option, } -/// Smart-constructor for an [Address](#Address) from a [verification key](#VerificationKey) hash. The resulting address has no delegation rights whatsoever. -pub fn from_verification_key(vk: Hash) -> Address { +/// Smart-constructor for an [Address](#Address) from a [verification key](#PublicKey) hash. The resulting address has no delegation rights whatsoever. +pub fn from_verification_key(vk: Hash) -> Address { Address { payment_credential: VerificationKeyCredential(vk), stake_credential: None, @@ -37,10 +37,10 @@ pub fn from_script(script: Hash) -> Address { } } -/// Set (or reset) the delegation part of an [Address](#Address) using a [verification key](#VerificationKey) hash. This is useful when combined with [`from_verification_key`](#from_verification_key) and/or [`from_script`](#from_script). +/// Set (or reset) the delegation part of an [Address](#Address) using a [verification key](#PublicKey) hash. This is useful when combined with [`from_verification_key`](#from_verification_key) and/or [`from_script`](#from_script). pub fn with_delegation_key( self: Address, - vk: Hash, + vk: Hash, ) -> Address { Address { payment_credential: self.payment_credential, @@ -69,7 +69,7 @@ pub type Referenced { Pointer { slot_number: Int, transaction_index: Int, certificate_index: Int } } -pub type VerificationKey = +pub type PublicKey = ByteArray pub type Script = @@ -80,11 +80,7 @@ pub type Signature = /// Verify an Ed25519 signature using the given verification key. /// Returns `True` when the signature is valid. -pub fn verify_signature( - key: VerificationKey, - msg: ByteArray, - sig: Signature, -) -> Bool { +pub fn verify_signature(key: PublicKey, msg: ByteArray, sig: Signature) -> Bool { builtin.verify_ed25519_signature(key, msg, sig) } @@ -107,16 +103,16 @@ pub type StakeCredential = pub type PaymentCredential = Credential -/// A unique stake pool identifier, as a hash of its owner verification key. pub type PoolId = - Hash + Hash -pub type PubKeyHash = - Hash +pub type PublicKeyHash = + Hash pub type ScriptHash = Hash +// DatumHash is taken as a Datum type so this may just need to change. pub type DatumHashDigest = Hash diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 4b9c942..74f854b 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -2,7 +2,7 @@ use aiken/dict.{Dict} use aiken/math/rational.{Rational} use aiken/transaction/credential.{ ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, - PubKeyHash, ScriptHash, + PublicKeyHash, ScriptHash, } use aiken/transaction/value.{Lovelace} @@ -13,9 +13,9 @@ pub type DRep { } pub type Delegatee { - DelegateStake(PubKeyHash) + DelegateStake(PublicKeyHash) DelegateVote(DRep) - DelegateBoth(PubKeyHash, DRep) + DelegateBoth(PublicKeyHash, DRep) } pub type Vote { @@ -29,7 +29,7 @@ pub type GovernanceActionId { index: Int, } -pub type ChangedParameters { +pub type ChangedProtocolParameters { new_parameters: Data, } @@ -85,5 +85,5 @@ pub type ProposalProcedure { pub type Voter { CommitteeVoter(HotCommitteeCredential) DRepVoter(DRepCredential) - StakePoolVoter(PubKeyHash) + StakePoolVoter(PublicKeyHash) } From 9f53607bf32756ae6a602b3b9f9b4e58279fa105 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 11 Mar 2024 12:58:16 -0700 Subject: [PATCH 18/30] changed the name from public key into verification key --- README.md | 4 ++-- lib/aiken/hash.ak | 2 +- lib/aiken/transaction.ak | 6 +++--- lib/aiken/transaction/certificate.ak | 6 +++--- lib/aiken/transaction/credential.ak | 24 ++++++++++++++---------- lib/aiken/transaction/governance.ak | 8 ++++---- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 1db4a2e..a33bf3f 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,10 @@ and aliases that make using Aiken a bliss. use aiken/hash.{Blake2b_224, Hash} use aiken/list use aiken/transaction.{ScriptContext} -use aiken/transaction/credential.{PublicKey} +use aiken/transaction/credential.{VerificationKey} pub type Datum { - owner: Hash, + owner: Hash, } pub type Redeemer { diff --git a/lib/aiken/hash.ak b/lib/aiken/hash.ak index 13d1c4d..4f86027 100644 --- a/lib/aiken/hash.ak +++ b/lib/aiken/hash.ak @@ -22,7 +22,7 @@ //// //// ```aiken //// pub type Credential { -//// VerificationKeyCredential(Hash) +//// VerificationKeyCredential(Hash) //// ScriptCredential(Hash) //// } //// ``` diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 1c6b26c..41426c9 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -7,8 +7,8 @@ use aiken/option use aiken/time.{PosixTime} use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ - Address, Credential, DatumHashDigest, PublicKeyHash, ScriptCredential, - ScriptHash, VerificationKeyCredential, + Address, Credential, DatumHashDigest, ScriptCredential, ScriptHash, + VerificationKeyCredential, VerificationKeyHash, } use aiken/transaction/governance.{ GovernanceActionId, ProposalProcedure, Vote, Voter, @@ -83,7 +83,7 @@ pub type Transaction { certificates: List, withdrawals: Pairs, validity_range: ValidityRange, - extra_signatories: List, + extra_signatories: List, redeemers: Pairs, datums: Dict, id: TransactionId, diff --git a/lib/aiken/transaction/certificate.ak b/lib/aiken/transaction/certificate.ak index 1e0ae27..177e766 100644 --- a/lib/aiken/transaction/certificate.ak +++ b/lib/aiken/transaction/certificate.ak @@ -1,6 +1,6 @@ use aiken/transaction/credential.{ ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, - PoolId, PublicKeyHash, + PoolId, VerificationKeyHash, } use aiken/transaction/governance.{Delegatee} use aiken/transaction/value.{Lovelace} @@ -29,10 +29,10 @@ pub type Certificate { // poolId PoolId, // pool VFR - PublicKeyHash, + VerificationKeyHash, ) // The retirement certificate and the Epoch in which the retirement will take place - PoolRetire(PublicKeyHash, Int) + PoolRetire(VerificationKeyHash, Int) // Authorize a Hot credential for a specific Committee member's cold credential AuthorizeHotCommittee(ColdCommitteeCredential, HotCommitteeCredential) ResignColdCommittee(ColdCommitteeCredential) diff --git a/lib/aiken/transaction/credential.ak b/lib/aiken/transaction/credential.ak index 3dab57e..99606cb 100644 --- a/lib/aiken/transaction/credential.ak +++ b/lib/aiken/transaction/credential.ak @@ -6,7 +6,7 @@ use aiken/hash.{Blake2b_224, Blake2b_256, Hash} /// Credentials are always one of two kinds: a direct public/private key /// pair, or a script (native or Plutus). pub type Credential { - VerificationKeyCredential(Hash) + VerificationKeyCredential(Hash) ScriptCredential(Hash) } @@ -21,8 +21,8 @@ pub type Address { stake_credential: Option, } -/// Smart-constructor for an [Address](#Address) from a [verification key](#PublicKey) hash. The resulting address has no delegation rights whatsoever. -pub fn from_verification_key(vk: Hash) -> Address { +/// Smart-constructor for an [Address](#Address) from a [verification key](#VerificationKey) hash. The resulting address has no delegation rights whatsoever. +pub fn from_verification_key(vk: Hash) -> Address { Address { payment_credential: VerificationKeyCredential(vk), stake_credential: None, @@ -37,10 +37,10 @@ pub fn from_script(script: Hash) -> Address { } } -/// Set (or reset) the delegation part of an [Address](#Address) using a [verification key](#PublicKey) hash. This is useful when combined with [`from_verification_key`](#from_verification_key) and/or [`from_script`](#from_script). +/// Set (or reset) the delegation part of an [Address](#Address) using a [verification key](#VerificationKey) hash. This is useful when combined with [`from_verification_key`](#from_verification_key) and/or [`from_script`](#from_script). pub fn with_delegation_key( self: Address, - vk: Hash, + vk: Hash, ) -> Address { Address { payment_credential: self.payment_credential, @@ -69,7 +69,7 @@ pub type Referenced { Pointer { slot_number: Int, transaction_index: Int, certificate_index: Int } } -pub type PublicKey = +pub type VerificationKey = ByteArray pub type Script = @@ -80,7 +80,11 @@ pub type Signature = /// Verify an Ed25519 signature using the given verification key. /// Returns `True` when the signature is valid. -pub fn verify_signature(key: PublicKey, msg: ByteArray, sig: Signature) -> Bool { +pub fn verify_signature( + key: VerificationKey, + msg: ByteArray, + sig: Signature, +) -> Bool { builtin.verify_ed25519_signature(key, msg, sig) } @@ -104,10 +108,10 @@ pub type PaymentCredential = Credential pub type PoolId = - Hash + Hash -pub type PublicKeyHash = - Hash +pub type VerificationKeyHash = + Hash pub type ScriptHash = Hash diff --git a/lib/aiken/transaction/governance.ak b/lib/aiken/transaction/governance.ak index 74f854b..fe969fd 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/aiken/transaction/governance.ak @@ -2,7 +2,7 @@ use aiken/dict.{Dict} use aiken/math/rational.{Rational} use aiken/transaction/credential.{ ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, - PublicKeyHash, ScriptHash, + ScriptHash, VerificationKeyHash, } use aiken/transaction/value.{Lovelace} @@ -13,9 +13,9 @@ pub type DRep { } pub type Delegatee { - DelegateStake(PublicKeyHash) + DelegateStake(VerificationKeyHash) DelegateVote(DRep) - DelegateBoth(PublicKeyHash, DRep) + DelegateBoth(VerificationKeyHash, DRep) } pub type Vote { @@ -85,5 +85,5 @@ pub type ProposalProcedure { pub type Voter { CommitteeVoter(HotCommitteeCredential) DRepVoter(DRepCredential) - StakePoolVoter(PublicKeyHash) + StakePoolVoter(VerificationKeyHash) } From dccb2a4dac7d2adf7768394b074952243fc79bea Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 11 Mar 2024 13:48:48 -0700 Subject: [PATCH 19/30] first rev for new naming convention --- lib/aiken/transaction.ak | 4 +-- lib/aiken/transaction/certificate.ak | 45 ++++++++++++++++++---------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 41426c9..2b71394 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -8,7 +8,7 @@ use aiken/time.{PosixTime} use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ Address, Credential, DatumHashDigest, ScriptCredential, ScriptHash, - VerificationKeyCredential, VerificationKeyHash, + StakeCredential, VerificationKeyCredential, VerificationKeyHash, } use aiken/transaction/governance.{ GovernanceActionId, ProposalProcedure, Vote, Voter, @@ -81,7 +81,7 @@ pub type Transaction { mint: Value, // ^ minted value is now back to a value certificates: List, - withdrawals: Pairs, + withdrawals: Pairs, validity_range: ValidityRange, extra_signatories: List, redeemers: Pairs, diff --git a/lib/aiken/transaction/certificate.ak b/lib/aiken/transaction/certificate.ak index 177e766..d90c05e 100644 --- a/lib/aiken/transaction/certificate.ak +++ b/lib/aiken/transaction/certificate.ak @@ -1,6 +1,6 @@ use aiken/transaction/credential.{ - ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, - PoolId, VerificationKeyHash, + ColdCommitteeCredential, DRepCredential, HotCommitteeCredential, PoolId, + StakeCredential, VerificationKeyHash, } use aiken/transaction/governance.{Delegatee} use aiken/transaction/value.{Lovelace} @@ -10,30 +10,43 @@ use aiken/transaction/value.{Lovelace} /// they require signatures from / specific keys. pub type Certificate { // Register staking credential with an optional deposit amount - RegisterStaking(Credential, Option) + RegisterStaking { + delegator: StakeCredential, + deposit_amount: Option, + } // Un-Register staking credential with an optional refund amount - UnRegisterStaking(Credential, Option) + UnregisterStaking { + delegator: StakeCredential, + refund_amount: Option, + } // Delegate staking credential to a Delegatee - DelegateStaking(Credential, Delegatee) + DelegateStaking { delegator: StakeCredential, delegatee: Delegatee } // Register and delegate staking credential to a Delegatee in one certificate. Noter that // deposit is mandatory. - RegisterAndDelegate(Credential, Delegatee, Lovelace) + RegisterAndDelegate { + delegator: StakeCredential, + delegatee: Delegatee, + deposit_amount: Lovelace, + } // Register a DRep with a deposit value. The optional anchor is omitted. - RegisterDRep(DRepCredential, Lovelace) + RegisterDRep { representative: DRepCredential, deposit_amount: Lovelace } // Update a DRep. The optional anchor is omitted. - UpdateDRep(DRepCredential) + UpdateDRep { representative: DRepCredential } // UnRegister a DRep with mandatory refund value - UnregisterDRep(DRepCredential, Lovelace) + UnregisterDRep { representative: DRepCredential, refund_amount: Lovelace } // A digest of the PoolParams - PoolRegister( + PoolRegister { // poolId - PoolId, + pool_id: PoolId, // pool VFR - VerificationKeyHash, - ) + pool_vrf: VerificationKeyHash, + } // The retirement certificate and the Epoch in which the retirement will take place - PoolRetire(VerificationKeyHash, Int) + PoolRetire { pool_id: PoolId, epoch: Int } // Authorize a Hot credential for a specific Committee member's cold credential - AuthorizeHotCommittee(ColdCommitteeCredential, HotCommitteeCredential) - ResignColdCommittee(ColdCommitteeCredential) + AuthorizeHotCommittee { + cold_memeber: ColdCommitteeCredential, + hot_member: HotCommitteeCredential, + } + ResignColdCommittee { cold_member: ColdCommitteeCredential } } From 4ead3a0c10c61a208dd900488afa94d9d05795f9 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 11 Mar 2024 14:23:15 -0700 Subject: [PATCH 20/30] Withdraw from now uses stake credential --- lib/aiken/transaction.ak | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 2b71394..816ef19 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -7,8 +7,8 @@ use aiken/option use aiken/time.{PosixTime} use aiken/transaction/certificate.{Certificate} use aiken/transaction/credential.{ - Address, Credential, DatumHashDigest, ScriptCredential, ScriptHash, - StakeCredential, VerificationKeyCredential, VerificationKeyHash, + Address, DatumHashDigest, ScriptCredential, ScriptHash, StakeCredential, + VerificationKeyCredential, VerificationKeyHash, } use aiken/transaction/governance.{ GovernanceActionId, ProposalProcedure, Vote, Voter, @@ -40,7 +40,7 @@ pub type ScriptPurpose { /// For scripts that validate reward withdrawals from a reward account. /// /// The argument identifies the target reward account. - WithdrawFrom(Credential) + WithdrawFrom(StakeCredential) /// Needed when delegating to a pool using stake credentials defined as a /// Plutus script. This purpose is also triggered when de-registering such /// stake credentials. From 0c71700068f41a218369eeaef049cb5b8cb8949d Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 24 Jun 2024 12:41:11 -0700 Subject: [PATCH 21/30] split scriptpurpose into scriptinfo, update names, and change scriptcontext --- lib/aiken/transaction.ak | 48 ++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 816ef19..3086fe4 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -24,7 +24,39 @@ use aiken/transaction/value.{Lovelace, PolicyId, Value} /// being executed. pub type ScriptContext { transaction: Transaction, - purpose: ScriptPurpose, + redeemer: Redeemer, + info: ScriptInfo, +} + +/// Characterizes the kind of script being executed. +pub type ScriptInfo { + /// For scripts executed as minting/burning policies, to insert + /// or remove assets from circulation. It's parameterized by the identifier + /// of the associated policy. + Minting(PolicyId) + /// For scripts that are used as payment credentials for addresses in + /// transaction outputs. They govern the rule by which the output they + /// reference can be spent. + Spending(OutputReference, Option) + /// For scripts that validate reward withdrawals from a reward account. + /// + /// The argument identifies the target reward account. + Withdrawing(StakeCredential) + /// Needed when delegating to a pool using stake credentials defined as a + /// Plutus script. This purpose is also triggered when de-registering such + /// stake credentials. + /// + /// The Int is a 0-based index of the given `TxCert` in `certificates`. + Publishing(Int, Certificate) + /// Voting for a type of voter using a governance action id to vote + /// yes / no / abstain inside a transaction. + /// + /// The voter is who is doing the governance action. + Voting(Voter) + /// Used to propose a governance action. + /// + /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. + Proposing(Int, ProposalProcedure) } /// Characterizes the kind of script being executed. @@ -40,30 +72,22 @@ pub type ScriptPurpose { /// For scripts that validate reward withdrawals from a reward account. /// /// The argument identifies the target reward account. - WithdrawFrom(StakeCredential) + Withdraw(StakeCredential) /// Needed when delegating to a pool using stake credentials defined as a /// Plutus script. This purpose is also triggered when de-registering such /// stake credentials. /// /// The Int is a 0-based index of the given `TxCert` in `certificates`. -<<<<<<< HEAD - Publish(Int, Certificate) - /// Voting for a type of voter using a governance action id to vote -||||||| parent of ee511e7 (Name changes and adding back in the yml workspace file) - Publish(Int, Certificate) - /// Voting for a type of voter using a governance action id to vote -======= Publish { certificate_index: Int, certificate: Certificate } /// Voting for a type of voter using a governance action id to vote ->>>>>>> ee511e7 (Name changes and adding back in the yml workspace file) /// yes / no / abstain inside a transaction. /// /// The voter is who is doing the governance action. - Voting(Voter) + Vote(Voter) /// Used to propose a governance action. /// /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. - Proposing { proposal_index: Int, proposal: ProposalProcedure } + Propose(Int, ProposalProcedure) } /// A Cardano `Transaction`, as seen by Plutus scripts. From c0db302129dffb713d739f594e253a40ee8960e4 Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 24 Jun 2024 12:45:24 -0700 Subject: [PATCH 22/30] merge this up into the v3 branch --- lib/aiken/transaction.ak | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 3086fe4..9c82a3a 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -19,7 +19,7 @@ use aiken/transaction/value.{Lovelace, PolicyId, Value} /// /// The context contains information about the entire transaction that contains /// the script. The transaction may also contain other scripts; to distinguish -/// between multiple scripts, the `ScriptContext` also contains a `purpose` +/// between multiple scripts, the `ScriptInfo` contains a `purpose` /// which indicates which script (or, for what purpose) of the transaction is /// being executed. pub type ScriptContext { @@ -28,7 +28,7 @@ pub type ScriptContext { info: ScriptInfo, } -/// Characterizes the kind of script being executed. +/// Characterizes the script information. pub type ScriptInfo { /// For scripts executed as minting/burning policies, to insert /// or remove assets from circulation. It's parameterized by the identifier @@ -59,7 +59,7 @@ pub type ScriptInfo { Proposing(Int, ProposalProcedure) } -/// Characterizes the kind of script being executed. +/// Characterizes the script purpose. pub type ScriptPurpose { /// For scripts executed as minting/burning policies, to insert /// or remove assets from circulation. It's parameterized by the identifier @@ -109,9 +109,10 @@ pub type Transaction { validity_range: ValidityRange, extra_signatories: List, redeemers: Pairs, + // ^ Pairs and ScriptPurpose are used here datums: Dict, id: TransactionId, - // all new fields below for v3 + // new fields for v3 votes: Dict>, proposal_procedures: List, current_treasury_amount: Option, From ae8be0fe95e7b80751774dc7881f272c1bb7bfdd Mon Sep 17 00:00:00 2001 From: logicalmechanism Date: Mon, 24 Jun 2024 13:00:00 -0700 Subject: [PATCH 23/30] withdraw should be Pairs --- lib/aiken/transaction.ak | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/aiken/transaction.ak b/lib/aiken/transaction.ak index 9c82a3a..558f667 100644 --- a/lib/aiken/transaction.ak +++ b/lib/aiken/transaction.ak @@ -114,6 +114,7 @@ pub type Transaction { id: TransactionId, // new fields for v3 votes: Dict>, + // ^ Does this need to be pairs? proposal_procedures: List, current_treasury_amount: Option, treasury_donation: Option, From c8e3b2f709f8137af189623818f9d5b7567e6b15 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 10 Aug 2024 23:45:39 +0200 Subject: [PATCH 24/30] re-organized modules. --- lib/aiken/{ => collection}/dict.ak | 0 lib/aiken/{ => collection}/list.ak | 4 +- lib/aiken/{ => collection}/pairs.ak | 0 lib/aiken/math/rational.ak | 2 +- lib/aiken/{ => primitive}/bytearray.ak | 0 lib/aiken/{ => primitive}/int.ak | 2 +- lib/aiken/{ => primitive}/string.ak | 0 lib/aiken/time.ak | 3 -- .../value.ak => cardano/assets.ak} | 6 +-- .../transaction => cardano}/certificate.ak | 6 +-- .../transaction => cardano}/credential.ak | 0 .../transaction => cardano}/governance.ak | 6 +-- lib/{aiken => cardano}/transaction.ak | 50 +++++++++---------- 13 files changed, 36 insertions(+), 43 deletions(-) rename lib/aiken/{ => collection}/dict.ak (100%) rename lib/aiken/{ => collection}/list.ak (99%) rename lib/aiken/{ => collection}/pairs.ak (100%) rename lib/aiken/{ => primitive}/bytearray.ak (100%) rename lib/aiken/{ => primitive}/int.ak (98%) rename lib/aiken/{ => primitive}/string.ak (100%) rename lib/{aiken/transaction/value.ak => cardano/assets.ak} (99%) rename lib/{aiken/transaction => cardano}/certificate.ak (94%) rename lib/{aiken/transaction => cardano}/credential.ak (100%) rename lib/{aiken/transaction => cardano}/governance.ak (94%) rename lib/{aiken => cardano}/transaction.ak (89%) diff --git a/lib/aiken/dict.ak b/lib/aiken/collection/dict.ak similarity index 100% rename from lib/aiken/dict.ak rename to lib/aiken/collection/dict.ak diff --git a/lib/aiken/list.ak b/lib/aiken/collection/list.ak similarity index 99% rename from lib/aiken/list.ak rename to lib/aiken/collection/list.ak index b8bb4b8..3745c49 100644 --- a/lib/aiken/list.ak +++ b/lib/aiken/collection/list.ak @@ -1,6 +1,6 @@ use aiken/builtin -use aiken/bytearray -use aiken/int +use aiken/primitive/bytearray +use aiken/primitive/int /// Determine if all elements of the list satisfy the given predicate. /// diff --git a/lib/aiken/pairs.ak b/lib/aiken/collection/pairs.ak similarity index 100% rename from lib/aiken/pairs.ak rename to lib/aiken/collection/pairs.ak diff --git a/lib/aiken/math/rational.ak b/lib/aiken/math/rational.ak index d99ef20..a5db89e 100644 --- a/lib/aiken/math/rational.ak +++ b/lib/aiken/math/rational.ak @@ -10,7 +10,7 @@ //// Comparing rational values should, therefore, only happen after reduction (see [reduce](#reduce)) or via the [compare](#compare) method. use aiken/builtin -use aiken/list +use aiken/collection/list use aiken/math use aiken/option diff --git a/lib/aiken/bytearray.ak b/lib/aiken/primitive/bytearray.ak similarity index 100% rename from lib/aiken/bytearray.ak rename to lib/aiken/primitive/bytearray.ak diff --git a/lib/aiken/int.ak b/lib/aiken/primitive/int.ak similarity index 98% rename from lib/aiken/int.ak rename to lib/aiken/primitive/int.ak index fd602b6..885f798 100644 --- a/lib/aiken/int.ak +++ b/lib/aiken/primitive/int.ak @@ -1,6 +1,6 @@ -use aiken/bytearray use aiken/math use aiken/option +use aiken/primitive/bytearray /// Compare two integers. /// diff --git a/lib/aiken/string.ak b/lib/aiken/primitive/string.ak similarity index 100% rename from lib/aiken/string.ak rename to lib/aiken/primitive/string.ak diff --git a/lib/aiken/time.ak b/lib/aiken/time.ak index f181815..e69de29 100644 --- a/lib/aiken/time.ak +++ b/lib/aiken/time.ak @@ -1,3 +0,0 @@ -/// A number of milliseconds since 00:00:00 UTC on 1 January 1970. -pub type PosixTime = - Int diff --git a/lib/aiken/transaction/value.ak b/lib/cardano/assets.ak similarity index 99% rename from lib/aiken/transaction/value.ak rename to lib/cardano/assets.ak index 7892c8e..f4e9556 100644 --- a/lib/aiken/transaction/value.ak +++ b/lib/cardano/assets.ak @@ -1,8 +1,8 @@ -use aiken/dict.{Dict, from_ascending_pairs_with} +use aiken/collection/dict.{Dict, from_ascending_pairs_with} +use aiken/collection/list use aiken/hash.{Blake2b_224, Hash} -use aiken/list use aiken/option -use aiken/transaction/credential.{Script} +use cardano/credential.{Script} /// Lovelace is now a type wrapper for Int. pub type Lovelace = diff --git a/lib/aiken/transaction/certificate.ak b/lib/cardano/certificate.ak similarity index 94% rename from lib/aiken/transaction/certificate.ak rename to lib/cardano/certificate.ak index d90c05e..2fb4098 100644 --- a/lib/aiken/transaction/certificate.ak +++ b/lib/cardano/certificate.ak @@ -1,9 +1,9 @@ -use aiken/transaction/credential.{ +use cardano/assets.{Lovelace} +use cardano/credential.{ ColdCommitteeCredential, DRepCredential, HotCommitteeCredential, PoolId, StakeCredential, VerificationKeyHash, } -use aiken/transaction/governance.{Delegatee} -use aiken/transaction/value.{Lovelace} +use cardano/governance.{Delegatee} /// An on-chain certificate attesting of some operation. Publishing /// certificates / triggers different kind of rules; most of the time, diff --git a/lib/aiken/transaction/credential.ak b/lib/cardano/credential.ak similarity index 100% rename from lib/aiken/transaction/credential.ak rename to lib/cardano/credential.ak diff --git a/lib/aiken/transaction/governance.ak b/lib/cardano/governance.ak similarity index 94% rename from lib/aiken/transaction/governance.ak rename to lib/cardano/governance.ak index fe969fd..c080c09 100644 --- a/lib/aiken/transaction/governance.ak +++ b/lib/cardano/governance.ak @@ -1,10 +1,10 @@ -use aiken/dict.{Dict} +use aiken/collection/dict.{Dict} use aiken/math/rational.{Rational} -use aiken/transaction/credential.{ +use cardano/assets.{Lovelace} +use cardano/credential.{ ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, ScriptHash, VerificationKeyHash, } -use aiken/transaction/value.{Lovelace} pub type DRep { FromCredential(DRepCredential) diff --git a/lib/aiken/transaction.ak b/lib/cardano/transaction.ak similarity index 89% rename from lib/aiken/transaction.ak rename to lib/cardano/transaction.ak index 558f667..8cf75b8 100644 --- a/lib/aiken/transaction.ak +++ b/lib/cardano/transaction.ak @@ -1,19 +1,21 @@ use aiken/builtin -use aiken/dict.{Dict} +use aiken/collection/dict.{Dict} +use aiken/collection/list use aiken/hash.{Blake2b_256, Hash, blake2b_256} use aiken/interval.{Interval} -use aiken/list use aiken/option -use aiken/time.{PosixTime} -use aiken/transaction/certificate.{Certificate} -use aiken/transaction/credential.{ +use cardano/assets.{Lovelace, PolicyId, Value} +use cardano/certificate.{Certificate} +use cardano/credential.{ Address, DatumHashDigest, ScriptCredential, ScriptHash, StakeCredential, VerificationKeyCredential, VerificationKeyHash, } -use aiken/transaction/governance.{ - GovernanceActionId, ProposalProcedure, Vote, Voter, -} -use aiken/transaction/value.{Lovelace, PolicyId, Value} +use cardano/governance.{GovernanceActionId, ProposalProcedure, Vote, Voter} + +/// An positive integer, that materializes the position of an element in a +/// collection. +type Index = + Int /// A context given to a script by the Cardano ledger when being executed. /// @@ -47,7 +49,7 @@ pub type ScriptInfo { /// stake credentials. /// /// The Int is a 0-based index of the given `TxCert` in `certificates`. - Publishing(Int, Certificate) + Publishing(Index, Certificate) /// Voting for a type of voter using a governance action id to vote /// yes / no / abstain inside a transaction. /// @@ -56,7 +58,7 @@ pub type ScriptInfo { /// Used to propose a governance action. /// /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. - Proposing(Int, ProposalProcedure) + Proposing(Index, ProposalProcedure) } /// Characterizes the script purpose. @@ -111,7 +113,7 @@ pub type Transaction { redeemers: Pairs, // ^ Pairs and ScriptPurpose are used here datums: Dict, - id: TransactionId, + id: Hash, // new fields for v3 votes: Dict>, // ^ Does this need to be pairs? @@ -128,9 +130,8 @@ pub type Transaction { /// ```aiken /// use aiken/transaction /// -/// transaction.placeholder().id == TransactionId { -/// hash: #"0000000000000000000000000000000000000000000000000000000000000000", -/// } +/// transaction.placeholder().id == +/// #"0000000000000000000000000000000000000000000000000000000000000000" /// /// transaction.placeholder().validity_range == interval.everything() /// ``` @@ -140,16 +141,14 @@ pub fn placeholder() -> Transaction { reference_inputs: [], outputs: [], fee: 0, - mint: value.zero(), + mint: assets.zero(), certificates: [], withdrawals: [], validity_range: interval.everything(), extra_signatories: [], redeemers: [], datums: dict.new(), - id: TransactionId { - hash: #"0000000000000000000000000000000000000000000000000000000000000000", - }, + id: #"0000000000000000000000000000000000000000000000000000000000000000", votes: dict.new(), proposal_procedures: [], current_treasury_amount: None, @@ -157,17 +156,14 @@ pub fn placeholder() -> Transaction { } } +/// A number of milliseconds since 00:00:00 UTC on 1 January 1970. +pub type PosixTime = + Int + /// An interval of POSIX time, measured in number milliseconds since 1970-01-01T00:00:00Z. pub type ValidityRange = Interval -/// A unique transaction identifier, as the hash of a transaction body. Note that the transaction id -/// isn't a direct hash of the `Transaction` as visible on-chain. Rather, they correspond to hash -/// digests of transaction body as they are serialized on the network. -pub type TransactionId { - hash: Hash, -} - /// An `Input` made of an output reference and, the resolved value associated with that output. pub type Input { output_reference: OutputReference, @@ -178,7 +174,7 @@ pub type Input { /// corresponds to the position in the output list of the transaction (identified by its id) /// that produced that output pub type OutputReference { - transaction_id: TransactionId, + transaction_id: Hash, output_index: Int, } From b6a3dfd392866092bb7cace49dff924f8295eb3d Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 11 Aug 2024 00:51:12 +0200 Subject: [PATCH 25/30] Adjust ProposalProcedure following end-to-end testing. --- lib/aiken/collection.ak | 4 ++ lib/cardano/certificate.ak | 19 +++--- lib/cardano/credential.ak | 30 ++-------- lib/cardano/governance.ak | 118 +++++++++++++++++++++++-------------- lib/cardano/transaction.ak | 20 ++----- 5 files changed, 96 insertions(+), 95 deletions(-) create mode 100644 lib/aiken/collection.ak diff --git a/lib/aiken/collection.ak b/lib/aiken/collection.ak new file mode 100644 index 0000000..39a8b23 --- /dev/null +++ b/lib/aiken/collection.ak @@ -0,0 +1,4 @@ +/// An positive integer, that materializes the position of an element in a +/// collection. +pub type Index = + Int diff --git a/lib/cardano/certificate.ak b/lib/cardano/certificate.ak index 2fb4098..ad1334f 100644 --- a/lib/cardano/certificate.ak +++ b/lib/cardano/certificate.ak @@ -1,8 +1,6 @@ use cardano/assets.{Lovelace} -use cardano/credential.{ - ColdCommitteeCredential, DRepCredential, HotCommitteeCredential, PoolId, - StakeCredential, VerificationKeyHash, -} +use cardano/credential.{Credential, + PoolId, StakeCredential, VerificationKeyHash} use cardano/governance.{Delegatee} /// An on-chain certificate attesting of some operation. Publishing @@ -29,11 +27,11 @@ pub type Certificate { deposit_amount: Lovelace, } // Register a DRep with a deposit value. The optional anchor is omitted. - RegisterDRep { representative: DRepCredential, deposit_amount: Lovelace } + RegisterDRep { representative: Credential, deposit_amount: Lovelace } // Update a DRep. The optional anchor is omitted. - UpdateDRep { representative: DRepCredential } + UpdateDRep { representative: Credential } // UnRegister a DRep with mandatory refund value - UnregisterDRep { representative: DRepCredential, refund_amount: Lovelace } + UnregisterDRep { representative: Credential, refund_amount: Lovelace } // A digest of the PoolParams PoolRegister { // poolId @@ -44,9 +42,6 @@ pub type Certificate { // The retirement certificate and the Epoch in which the retirement will take place PoolRetire { pool_id: PoolId, epoch: Int } // Authorize a Hot credential for a specific Committee member's cold credential - AuthorizeHotCommittee { - cold_memeber: ColdCommitteeCredential, - hot_member: HotCommitteeCredential, - } - ResignColdCommittee { cold_member: ColdCommitteeCredential } + AuthorizeHotCommittee { cold_memeber: Credential, hot_member: Credential } + ResignColdCommittee { cold_member: Credential } } diff --git a/lib/cardano/credential.ak b/lib/cardano/credential.ak index 99606cb..4b00867 100644 --- a/lib/cardano/credential.ak +++ b/lib/cardano/credential.ak @@ -6,8 +6,8 @@ use aiken/hash.{Blake2b_224, Blake2b_256, Hash} /// Credentials are always one of two kinds: a direct public/private key /// pair, or a script (native or Plutus). pub type Credential { - VerificationKeyCredential(Hash) - ScriptCredential(Hash) + VerificationKey(VerificationKeyHash) + Script(ScriptHash) } /// A Cardano `Address` typically holding one or two credential references. @@ -23,18 +23,12 @@ pub type Address { /// Smart-constructor for an [Address](#Address) from a [verification key](#VerificationKey) hash. The resulting address has no delegation rights whatsoever. pub fn from_verification_key(vk: Hash) -> Address { - Address { - payment_credential: VerificationKeyCredential(vk), - stake_credential: None, - } + Address { payment_credential: VerificationKey(vk), stake_credential: None } } /// Smart-constructor for an [Address](#Address) from a [script](#Script) hash. The address has no delegation rights whatsoever. pub fn from_script(script: Hash) -> Address { - Address { - payment_credential: ScriptCredential(script), - stake_credential: None, - } + Address { payment_credential: Script(script), stake_credential: None } } /// Set (or reset) the delegation part of an [Address](#Address) using a [verification key](#VerificationKey) hash. This is useful when combined with [`from_verification_key`](#from_verification_key) and/or [`from_script`](#from_script). @@ -44,7 +38,7 @@ pub fn with_delegation_key( ) -> Address { Address { payment_credential: self.payment_credential, - stake_credential: Some(Inline(VerificationKeyCredential(vk))), + stake_credential: Some(Inline(VerificationKey(vk))), } } @@ -55,7 +49,7 @@ pub fn with_delegation_script( ) -> Address { Address { payment_credential: self.payment_credential, - stake_credential: Some(Inline(ScriptCredential(script))), + stake_credential: Some(Inline(Script(script))), } } @@ -119,15 +113,3 @@ pub type ScriptHash = // DatumHash is taken as a Datum type so this may just need to change. pub type DatumHashDigest = Hash - -pub type DRepCredential { - member: Credential, -} - -pub type ColdCommitteeCredential { - member: Credential, -} - -pub type HotCommitteeCredential { - member: Credential, -} diff --git a/lib/cardano/governance.ak b/lib/cardano/governance.ak index c080c09..df5a6b2 100644 --- a/lib/cardano/governance.ak +++ b/lib/cardano/governance.ak @@ -1,13 +1,10 @@ -use aiken/collection/dict.{Dict} +use aiken/collection.{Index} use aiken/math/rational.{Rational} use cardano/assets.{Lovelace} -use cardano/credential.{ - ColdCommitteeCredential, Credential, DRepCredential, HotCommitteeCredential, - ScriptHash, VerificationKeyHash, -} +use cardano/credential.{Credential, ScriptHash, VerificationKeyHash} pub type DRep { - FromCredential(DRepCredential) + FromCredential(Credential) AlwaysAbstain AlwaysNoConfidence } @@ -26,12 +23,11 @@ pub type Vote { pub type GovernanceActionId { transaction_id: ByteArray, - index: Int, + proposal_procedure: Index, } -pub type ChangedProtocolParameters { - new_parameters: Data, -} +pub type ProtocolParametersUpdate = + Data pub type ProtocolVersion { major: Int, @@ -39,43 +35,77 @@ pub type ProtocolVersion { } pub type Constitution { - guardrails_script: Option, + guardrails: Option, } pub type GovernanceAction { - // - ProtocolParametersChange( - Option, - ChangedProtocolParameters, - // Hash of the constitution script - Option, - ) - // proposal to update protocol version - HardForkInitiation(Option, ProtocolVersion) - // - TreasuryWithdrawals( - Dict, - // Hash of the constitution script - Option, - ) - // - NoConfidence(Option) - // - UpdateCommittee( - Option, - // Committee members to be removed - List, - // Committee members to be added - Dict, - // New quorum - Rational, - ) - // - NewConstitution(Option, Constitution) - // - InfoAction + ProtocolParameters { + /// The last governance action of type 'ProtocolParameters'. They must all + /// form a chain. + ancestor: Option, + /// The new proposed protocol parameters. Only values set to `Some` are relevant. + new_parameters: ProtocolParametersUpdate, + /// The optional guardrails script defined in the constitution. The script + /// is executed by the ledger in addition to the hard-coded ledger rules. + /// + /// It must pass for the new protocol parameters to be deemed valid. + guardrails: Option, + } + Hardfork { + /// The last governance action of type `HardFork`. They must all + /// form a chain. + ancestor: Option, + /// The new proposed version. Few rules apply to proposing new versions: + /// + /// - The `major` component, if incremented, must be exactly one more than the current. + /// - The `minor` component, if incremented, must be exactly one more than the current. + /// - If the `major` component is incremented, `minor` must be set to `0`. + /// - Neither `minor` nor `major` can be decremented. + new_version: ProtocolVersion, + } + TreasuryWithdrawal { + /// A collection of beneficiaries, which can be plain verification key + /// hashes or script hashes (e.g. DAO). + beneficiaries: Pairs, + /// The optional guardrails script defined in the constitution. The script + /// is executed by the ledger in addition to the hard-coded ledger rules. + /// + /// It must pass for the withdrawals to be authorized. + guardrails: Option, + } + NoConfidence { + /// The last governance action of type `NoConfidence` or + /// `ConstitutionalCommittee`. They must all / form a chain. + ancestor: Option, + } + ConstitutionalCommittee { + /// The last governance action of type `NoConfidence` or + /// `ConstitutionalCommittee`. They must all / form a chain. + ancestor: Option, + /// Constitutional members to be removed. + evicted_members: List, + /// Constitutional members to be added. + added_members: Pairs, + /// The new quorum value, as a ratio of a numerator and a denominator. The + /// quorum specifies the threshold of 'Yes' votes necessary for the + /// constitutional committee to accept a proposal procedure. + quorum: Rational, + } + NewConstitution { + /// The last governance action of type `Constitution` or + /// `ConstitutionalCommittee`. They must all / form a chain. + ancestor: Option, + /// The new proposed constitution. + constitution: Constitution, + } + NicePoll } +/// An epoch number after which constitutional committee member +/// mandate expires. +pub type Mandate = + Int + pub type ProposalProcedure { deposit: Lovelace, return_address: Credential, @@ -83,7 +113,7 @@ pub type ProposalProcedure { } pub type Voter { - CommitteeVoter(HotCommitteeCredential) - DRepVoter(DRepCredential) + CommitteeVoter(Credential) + DRepVoter(Credential) StakePoolVoter(VerificationKeyHash) } diff --git a/lib/cardano/transaction.ak b/lib/cardano/transaction.ak index 8cf75b8..678b535 100644 --- a/lib/cardano/transaction.ak +++ b/lib/cardano/transaction.ak @@ -1,4 +1,5 @@ use aiken/builtin +use aiken/collection.{Index} use aiken/collection/dict.{Dict} use aiken/collection/list use aiken/hash.{Blake2b_256, Hash, blake2b_256} @@ -7,16 +8,11 @@ use aiken/option use cardano/assets.{Lovelace, PolicyId, Value} use cardano/certificate.{Certificate} use cardano/credential.{ - Address, DatumHashDigest, ScriptCredential, ScriptHash, StakeCredential, - VerificationKeyCredential, VerificationKeyHash, + Address, DatumHashDigest, Script, ScriptHash, StakeCredential, VerificationKey, + VerificationKeyHash, } use cardano/governance.{GovernanceActionId, ProposalProcedure, Vote, Voter} -/// An positive integer, that materializes the position of an element in a -/// collection. -type Index = - Int - /// A context given to a script by the Cardano ledger when being executed. /// /// The context contains information about the entire transaction that contains @@ -103,20 +99,15 @@ pub type Transaction { reference_inputs: List, outputs: List, fee: Lovelace, - // ^ the fee is now lovelace mint: Value, - // ^ minted value is now back to a value certificates: List, withdrawals: Pairs, validity_range: ValidityRange, extra_signatories: List, redeemers: Pairs, - // ^ Pairs and ScriptPurpose are used here datums: Dict, id: Hash, - // new fields for v3 votes: Dict>, - // ^ Does this need to be pairs? proposal_procedures: List, current_treasury_amount: Option, treasury_donation: Option, @@ -268,9 +259,8 @@ pub fn find_script_outputs( |> list.filter( fn(output) { when output.address.payment_credential is { - ScriptCredential(addr_script_hash) -> - script_hash == addr_script_hash - VerificationKeyCredential(_) -> False + Script(addr_script_hash) -> script_hash == addr_script_hash + VerificationKey(_) -> False } }, ) From 70075756c214edbedc3ce20d146540c76e0346d6 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 11 Aug 2024 18:32:52 +0200 Subject: [PATCH 26/30] Support protocol parameters update, with access functions. Unfortunately, we cannot simply map those as a large record because it is encoded as a cbor Map, and not as a constructor. We can't also just provide it as a Dict or Pairs because the resulting types are: (1) non-canonical (rationales are encoded as arrays instead of a constructor with two fields, unlike in other places....) (2) heterogeneous (protocol parameters have different types). Providing accessors like so will play nicely with future updates as well. One big downside: we traverse the map for every access x_x ... In the long-run, we might to provide some method to fold over parameters and traverse the map only _once_. This would require to temporarily store parameters inside an ADT which users can pattern-match on at the call site. ``` pub fn fold(self, zero: b, with: fn(ProtocolParameter, b) -> b) -> b { ... } pub type ProtocolParameter { MinFeeCoefficient(Int), MinFeeConstant(Int), ... } ``` This would provide some kind of optimal traversal of the parameters, modulo the creation of the intermediate ADT. Having the ability to use case/constr here would be nice. --- lib/cardano/governance.ak | 78 ++-- lib/cardano/governance/protocol_parameters.ak | 368 ++++++++++++++++++ 2 files changed, 406 insertions(+), 40 deletions(-) create mode 100644 lib/cardano/governance/protocol_parameters.ak diff --git a/lib/cardano/governance.ak b/lib/cardano/governance.ak index df5a6b2..fee48e3 100644 --- a/lib/cardano/governance.ak +++ b/lib/cardano/governance.ak @@ -2,40 +2,12 @@ use aiken/collection.{Index} use aiken/math/rational.{Rational} use cardano/assets.{Lovelace} use cardano/credential.{Credential, ScriptHash, VerificationKeyHash} +use cardano/governance/protocol_parameters.{ProtocolParametersUpdate} -pub type DRep { - FromCredential(Credential) - AlwaysAbstain - AlwaysNoConfidence -} - -pub type Delegatee { - DelegateStake(VerificationKeyHash) - DelegateVote(DRep) - DelegateBoth(VerificationKeyHash, DRep) -} - -pub type Vote { - No - Yes - Abstain -} - -pub type GovernanceActionId { - transaction_id: ByteArray, - proposal_procedure: Index, -} - -pub type ProtocolParametersUpdate = - Data - -pub type ProtocolVersion { - major: Int, - minor: Int, -} - -pub type Constitution { - guardrails: Option, +pub type ProposalProcedure { + deposit: Lovelace, + return_address: Credential, + governance_action: GovernanceAction, } pub type GovernanceAction { @@ -51,7 +23,7 @@ pub type GovernanceAction { /// It must pass for the new protocol parameters to be deemed valid. guardrails: Option, } - Hardfork { + HardFork { /// The last governance action of type `HardFork`. They must all /// form a chain. ancestor: Option, @@ -101,17 +73,43 @@ pub type GovernanceAction { NicePoll } +pub type DRep { + FromCredential(Credential) + AlwaysAbstain + AlwaysNoConfidence +} + +pub type Delegatee { + DelegateStake(VerificationKeyHash) + DelegateVote(DRep) + DelegateBoth(VerificationKeyHash, DRep) +} + +pub type Vote { + No + Yes + Abstain +} + +pub type GovernanceActionId { + transaction_id: ByteArray, + proposal_procedure: Index, +} + +pub type ProtocolVersion { + major: Int, + minor: Int, +} + +pub type Constitution { + guardrails: Option, +} + /// An epoch number after which constitutional committee member /// mandate expires. pub type Mandate = Int -pub type ProposalProcedure { - deposit: Lovelace, - return_address: Credential, - governance_action: GovernanceAction, -} - pub type Voter { CommitteeVoter(Credential) DRepVoter(Credential) diff --git a/lib/cardano/governance/protocol_parameters.ak b/lib/cardano/governance/protocol_parameters.ak new file mode 100644 index 0000000..ca2c575 --- /dev/null +++ b/lib/cardano/governance/protocol_parameters.ak @@ -0,0 +1,368 @@ +use aiken/math/rational.{Rational} +use cardano/assets.{Lovelace} + +pub opaque type ProtocolParametersUpdate { + inner: Pairs, +} + +pub type ScriptExecutionPrices { + memory: Rational, + cpu: Rational, +} + +pub type ExecutionUnits { + memory: Int, + cpu: Int, +} + +pub type StakePoolOperatorVotingThresholds { + motion_of_no_confidence: Rational, + constitutional_committee: ConstitutionalCommitteeThresholds, + hard_fork: Rational, + protocol_parameters: ProtocolParametersThresholds< + Rational, + Void, + Void, + Void, + Void, + >, +} + +pub type DelegateRepresentativeVotingThresholds { + motion_of_no_confidence: Rational, + constitutional_committee: ConstitutionalCommitteeThresholds, + constitution: Rational, + hard_fork: Rational, + protocol_parameters: ProtocolParametersThresholds< + Void, + Rational, + Rational, + Rational, + Rational, + >, + treasury_withdrawal: Rational, +} + +pub type ProtocolParametersThresholds< + security, + network, + economic, + technical, + governance, +> { + security_group: security, + network_group: network, + economic_group: economic, + technical_group: technical, + governance_group: governance, +} + +pub type ConstitutionalCommitteeThresholds { + default: Rational, + under_no_confidence: Rational, +} + +/// The linear coefficient that intervenes in the transaction fee calculation. +/// It is multiplied by the size of the transaction in bytes to obtain a Lovelace value. +pub fn min_fee_coefficient(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 0, into_int) +} + +/// The constant factor that intervenes in the transaction fee calculation. It is +/// a flat cost of lovelace that is added to every fee calculation. +pub fn min_fee_constant(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 1, into_int) +} + +/// The maximum size of a serialized block body, expressed in bytes. +pub fn max_block_body_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 2, into_int) +} + +/// The maximum size of a serialized transaction (body + witnesses), expressed in bytes. +pub fn max_transaction_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 3, into_int) +} + +/// The maximum size of a serialized block header, expressed in bytes. +pub fn max_block_header_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 4, into_int) +} + +/// The required deposit amount when registering stake credentials, expressed in Lovelace. +pub fn stake_credential_deposit( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 5, into_int) +} + +/// The required deposit amount when registering a stake pool, expressed in Lovelace. +pub fn stake_pool_deposit(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 6, into_int) +} + +/// The maximum number of epoch in the future allowed for a stake pool retirement to be scheduled. +pub fn stake_pool_retirement_horizon( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 7, into_int) +} + +/// The desired/optimal number of fully saturated stake pools in the system. Also known as the _'k-parameter'_. +pub fn desired_number_of_stake_pools( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 8, into_int) +} + +/// A parameter controlling the influence of an pool owner's pledge on the rewards. Also known as _'a0'_. +pub fn stake_pool_pledge_influence( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 9, into_rational) +} + +/// The monetary expansion parameter, controlling the fraction of Ada put in circulation on every epoch through the incentivies model. Also known as _'ρ'_. +pub fn monetary_expansion(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 10, into_rational) +} + +/// The parameter controlling what fraction (%) of available rewards is sent to the treasury on every epoch. Also known as _'τ'_. +pub fn treasury_expansion(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 11, into_rational) +} + +/// Minimum authorized constant cost that stake pools can declare when registering, expressed in Lovelace. +pub fn min_stake_pool_cost(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 16, into_int) +} + +/// The linear coefficient that intervenes in the calculation of the minimum Ada value that any UTxO must hold. It is expressed in Lovelace per Byte, and is also known as the 'coins per utxo byte' parameter. +pub fn min_utxo_deposit_coefficient( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 17, into_int) +} + +/// The costs associated with the various operations of the Plutus Virtual Machine, which can be different for each Plutus version. +pub fn cost_models(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 18, identity) +} + +/// The price, in Lovelace per unit, of the execution units corresponding to cpu and memory usage of on-chain scripts. +pub fn script_execution_prices( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 19, into_script_execution_prices) +} + +/// The maximum execution units allowed for a single transaction. +pub fn max_transaction_execution_units( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 20, into_execution_units) +} + +/// The maximum execution units allowed for a single block. +pub fn max_block_execution_units( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 21, into_execution_units) +} + +/// The maximum size of a serialized value in a transaction output. This effectively limits +/// the maximum kinds of assets that can be sent in a single output. It is expressed in bytes. +pub fn max_value_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 22, into_int) +} + +/// The scaling factor applied to the transaction cost for defining the minimum collateral +/// amount. It is expressed in percent points (so 100 = 100%). +pub fn collateral_percentage(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 23, into_int) +} + +/// The maximum number of collateral inputs allowed in the transaction. +pub fn max_collateral_inputs(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 24, into_int) +} + +/// The various governance voting thresholds pertaining to stake pool operators. +pub fn stake_pool_operator_voting_thresholds( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 25, into_spo_voting_thresholds) +} + +/// The various governance voting thresholds pertaining to delegate representatives +/// (a.k.a DReps). +pub fn delegate_representative_voting_thresholds( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 26, into_drep_voting_thresholds) +} + +/// The minimum number of members in the constitutional committee. Any updates of the committee +/// must leave at least this number of members. +pub fn min_constitutional_committee_size( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 27, into_int) +} + +/// The maximum length of a constitutional committee member, expressed in number of epochs. +pub fn max_constitutional_committee_mandate( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 28, into_int) +} + +/// The lifetime of any governance proposal. An action that hasn't been approved beyond that +/// period is considered inactive and discarded. It is expressed in number of epochs. +pub fn governance_proposal_lifetime( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 29, into_int) +} + +/// The required deposit amount for governance proposal procedures, expressed in Lovelace. +pub fn governance_proposal_deposit( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 30, into_int) +} + +/// The required deposit amount when registering as a delegate representative, expressed in +/// Lovelace. +pub fn delegate_representative_deposit( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 31, into_int) +} + +/// The maximum number of epochs that a delegate representative can stay inactive (i.e. no +/// voting) without becoming _inactive_ and removed from thresholds calculations. +pub fn delegate_representative_max_idle_time( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 32, into_int) +} + +/// The base tier fee coefficient for reference scripts. Reference scripts gets increasingly +/// more expensives every ~24KB, the base coefficient is a multiplicating factor which grows +/// exponentially with each tier. +pub fn reference_scripts_tier_fee_initial_factor( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 33, into_rational) +} + +// Internals ------------------------------------------------------------------- + +type ProtocolParametersIndex = + Int + +fn get_protocol_param( + self: Pairs, + ix: ProtocolParametersIndex, + into: fn(Data) -> a, +) -> Option { + when self is { + [] -> None + [Pair(jx, param), ..tail] -> + if ix == jx { + Some(into(param)) + } else { + get_protocol_param(tail, ix, into) + } + } +} + +fn into_int(param: Data) -> Int { + expect param: Int = param + param +} + +fn into_rational(param: Data) -> Rational { + expect [numerator, denominator]: List = param + expect Some(r) = rational.new(numerator, denominator) + r +} + +fn into_execution_units(param: Data) -> ExecutionUnits { + expect [memory, cpu]: List = param + ExecutionUnits { memory, cpu } +} + +fn into_script_execution_prices(param: Data) -> ScriptExecutionPrices { + expect [memory, cpu]: List = param + let memory = into_rational(memory) + let cpu = into_rational(cpu) + ScriptExecutionPrices { memory, cpu } +} + +fn into_spo_voting_thresholds(param: Data) -> StakePoolOperatorVotingThresholds { + expect [ + motion_of_no_confidence, + constitutional_committee, + constitutional_committee_under_no_confidence, + hard_fork, + protocol_parameters_security_group, + ]: List = param + + StakePoolOperatorVotingThresholds { + motion_of_no_confidence: into_rational(motion_of_no_confidence), + constitutional_committee: ConstitutionalCommitteeThresholds { + default: into_rational(constitutional_committee), + under_no_confidence: into_rational( + constitutional_committee_under_no_confidence, + ), + }, + hard_fork: into_rational(hard_fork), + protocol_parameters: ProtocolParametersThresholds { + security_group: into_rational(protocol_parameters_security_group), + network_group: Void, + economic_group: Void, + technical_group: Void, + governance_group: Void, + }, + } +} + +fn into_drep_voting_thresholds( + param: Data, +) -> DelegateRepresentativeVotingThresholds { + trace @"drep voting thresholds": param + expect [ + motion_of_no_confidence, + constitutional_committee, + constitutional_committee_under_no_confidence, + constitution, + hard_fork, + protocol_parameters_network_group, + protocol_parameters_economic_group, + protocol_parameters_technical_group, + protocol_parameters_governance_group, + treasury_withdrawal, + ]: List = param + + DelegateRepresentativeVotingThresholds { + motion_of_no_confidence: into_rational(motion_of_no_confidence), + constitutional_committee: ConstitutionalCommitteeThresholds { + default: into_rational(constitutional_committee), + under_no_confidence: into_rational( + constitutional_committee_under_no_confidence, + ), + }, + constitution: into_rational(constitution), + hard_fork: into_rational(hard_fork), + protocol_parameters: ProtocolParametersThresholds { + security_group: Void, + network_group: into_rational(protocol_parameters_network_group), + economic_group: into_rational(protocol_parameters_economic_group), + technical_group: into_rational(protocol_parameters_technical_group), + governance_group: into_rational(protocol_parameters_governance_group), + }, + treasury_withdrawal: into_rational(treasury_withdrawal), + } +} From 9e12bbf060699864f96580d1c6063a5fe9de687b Mon Sep 17 00:00:00 2001 From: KtorZ Date: Mon, 12 Aug 2024 02:10:44 +0200 Subject: [PATCH 27/30] Rework and fix Conway certificates. --- lib/cardano/certificate.ak | 94 ++++++++++++++++++++++++-------------- lib/cardano/credential.ak | 5 +- lib/cardano/governance.ak | 18 ++------ lib/cardano/transaction.ak | 10 ++-- 4 files changed, 69 insertions(+), 58 deletions(-) diff --git a/lib/cardano/certificate.ak b/lib/cardano/certificate.ak index ad1334f..6ca9045 100644 --- a/lib/cardano/certificate.ak +++ b/lib/cardano/certificate.ak @@ -1,47 +1,71 @@ use cardano/assets.{Lovelace} -use cardano/credential.{Credential, - PoolId, StakeCredential, VerificationKeyHash} -use cardano/governance.{Delegatee} +use cardano/credential.{Credential, StakePoolId, VerificationKeyHash} /// An on-chain certificate attesting of some operation. Publishing /// certificates / triggers different kind of rules; most of the time, /// they require signatures from / specific keys. pub type Certificate { - // Register staking credential with an optional deposit amount - RegisterStaking { - delegator: StakeCredential, - deposit_amount: Option, + // Register a stake credential with an optional deposit amount. + // The deposit is always present when using the new registration certificate + // format available since the Conway era. + RegisterCredential { credential: Credential, deposit: Option } + // Un-Register a stake credential with an optional refund amount + // The deposit is always present when using the new de-registration certificate + // format available since the Conway era. + UnregisterCredential { credential: Credential, refund: Option } + // Delegate stake to a [Delegate](#Delegate). + DelegateCredential { credential: Credential, delegate: Delegate } + // Register and delegate staking credential to a Delegatee in one certificate. + RegisterAndDelegateCredential { + credential: Credential, + delegate: Delegate, + deposit: Lovelace, } - // Un-Register staking credential with an optional refund amount - UnregisterStaking { - delegator: StakeCredential, - refund_amount: Option, + // Register a delegate representative (a.k.a DRep). The deposit is explicit and + // is refunded when the delegate steps down (unregister). + RegisterDelegateRepresentative { + delegate_representative: Credential, + deposit: Lovelace, } - // Delegate staking credential to a Delegatee - DelegateStaking { delegator: StakeCredential, delegatee: Delegatee } - // Register and delegate staking credential to a Delegatee in one certificate. Noter that - // deposit is mandatory. - RegisterAndDelegate { - delegator: StakeCredential, - delegatee: Delegatee, - deposit_amount: Lovelace, + // Update a delegate representative (a.k.a DRep). The certificate also contains + // metadata which aren't visible on-chain. + UpdateDelegateRepresentative { delegate_representative: Credential } + // UnRegister a delegate representative, and refund back its past deposit. + UnregisterDelegateRepresentative { + delegate_representative: Credential, + refund: Lovelace, } - // Register a DRep with a deposit value. The optional anchor is omitted. - RegisterDRep { representative: Credential, deposit_amount: Lovelace } - // Update a DRep. The optional anchor is omitted. - UpdateDRep { representative: Credential } - // UnRegister a DRep with mandatory refund value - UnregisterDRep { representative: Credential, refund_amount: Lovelace } - // A digest of the PoolParams - PoolRegister { - // poolId - pool_id: PoolId, - // pool VFR - pool_vrf: VerificationKeyHash, + // Register a new stake pool + RegisterStakePool { + // The hash digest of the stake pool's cold (public) key + stake_pool: StakePoolId, + // The hash digest of the stake pool's VRF (public) key + vrf: VerificationKeyHash, } - // The retirement certificate and the Epoch in which the retirement will take place - PoolRetire { pool_id: PoolId, epoch: Int } + // Retire a stake pool. 'at_epoch' indicates in which the retirement will take place + RetireStakePool { stake_pool: StakePoolId, at_epoch: Int } // Authorize a Hot credential for a specific Committee member's cold credential - AuthorizeHotCommittee { cold_memeber: Credential, hot_member: Credential } - ResignColdCommittee { cold_member: Credential } + AuthorizeConstitutionalCommitteeProxy { + constitutional_committee_member: Credential, + proxy: Credential, + } + // Step down from the constitutional committee as a member. + RetireFromConstitutionalCommittee { + constitutional_committee_member: Credential, + } +} + +pub type Delegate { + DelegateBlockProduction { stake_pool: StakePoolId } + DelegateVote { delegate_representative: DelegateRepresentative } + DelegateBoth { + stake_pool: StakePoolId, + delegate_representative: DelegateRepresentative, + } +} + +pub type DelegateRepresentative { + Registered(Credential) + AlwaysAbstain + AlwaysNoConfidence } diff --git a/lib/cardano/credential.ak b/lib/cardano/credential.ak index 4b00867..4cd8fbf 100644 --- a/lib/cardano/credential.ak +++ b/lib/cardano/credential.ak @@ -101,7 +101,7 @@ pub type StakeCredential = pub type PaymentCredential = Credential -pub type PoolId = +pub type StakePoolId = Hash pub type VerificationKeyHash = @@ -110,6 +110,5 @@ pub type VerificationKeyHash = pub type ScriptHash = Hash -// DatumHash is taken as a Datum type so this may just need to change. -pub type DatumHashDigest = +pub type DatumHash = Hash diff --git a/lib/cardano/governance.ak b/lib/cardano/governance.ak index fee48e3..3d108ac 100644 --- a/lib/cardano/governance.ak +++ b/lib/cardano/governance.ak @@ -73,18 +73,6 @@ pub type GovernanceAction { NicePoll } -pub type DRep { - FromCredential(Credential) - AlwaysAbstain - AlwaysNoConfidence -} - -pub type Delegatee { - DelegateStake(VerificationKeyHash) - DelegateVote(DRep) - DelegateBoth(VerificationKeyHash, DRep) -} - pub type Vote { No Yes @@ -111,7 +99,7 @@ pub type Mandate = Int pub type Voter { - CommitteeVoter(Credential) - DRepVoter(Credential) - StakePoolVoter(VerificationKeyHash) + ConstitutionalCommitteeMember(Credential) + DelegateRepresentative(Credential) + StakePool(VerificationKeyHash) } diff --git a/lib/cardano/transaction.ak b/lib/cardano/transaction.ak index 678b535..ca0c6e9 100644 --- a/lib/cardano/transaction.ak +++ b/lib/cardano/transaction.ak @@ -8,7 +8,7 @@ use aiken/option use cardano/assets.{Lovelace, PolicyId, Value} use cardano/certificate.{Certificate} use cardano/credential.{ - Address, DatumHashDigest, Script, ScriptHash, StakeCredential, VerificationKey, + Address, DatumHash, Script, ScriptHash, StakeCredential, VerificationKey, VerificationKeyHash, } use cardano/governance.{GovernanceActionId, ProposalProcedure, Vote, Voter} @@ -105,7 +105,7 @@ pub type Transaction { validity_range: ValidityRange, extra_signatories: List, redeemers: Pairs, - datums: Dict, + datums: Dict, id: Hash, votes: Dict>, proposal_procedures: List, @@ -181,7 +181,7 @@ pub type Output { pub type Datum { NoDatum /// A datum referenced by its hash digest. - DatumHash(DatumHashDigest) + DatumHash(DatumHash) /// A datum completely inlined in the output. InlineDatum(Data) } @@ -221,8 +221,8 @@ pub fn find_input( /// witnesses. pub fn find_datum( outputs: List, - datums: Dict, - datum_hash: DatumHashDigest, + datums: Dict, + datum_hash: DatumHash, ) -> Option { datums |> dict.get(datum_hash) From 9744a4036d720c4ac537b8af9ff91a9dc8c75891 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Tue, 13 Aug 2024 17:21:21 +0200 Subject: [PATCH 28/30] Fix votes type in transaction context. --- lib/cardano/governance.ak | 6 +++++- lib/cardano/transaction.ak | 12 +++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/cardano/governance.ak b/lib/cardano/governance.ak index 3d108ac..b01d81e 100644 --- a/lib/cardano/governance.ak +++ b/lib/cardano/governance.ak @@ -1,4 +1,5 @@ use aiken/collection.{Index} +use aiken/hash.{Blake2b_256, Hash} use aiken/math/rational.{Rational} use cardano/assets.{Lovelace} use cardano/credential.{Credential, ScriptHash, VerificationKeyHash} @@ -79,8 +80,11 @@ pub type Vote { Abstain } +pub type TransactionId = + Hash + pub type GovernanceActionId { - transaction_id: ByteArray, + transaction: TransactionId, proposal_procedure: Index, } diff --git a/lib/cardano/transaction.ak b/lib/cardano/transaction.ak index ca0c6e9..9e45e53 100644 --- a/lib/cardano/transaction.ak +++ b/lib/cardano/transaction.ak @@ -13,6 +13,9 @@ use cardano/credential.{ } use cardano/governance.{GovernanceActionId, ProposalProcedure, Vote, Voter} +pub type TransactionId = + Hash + /// A context given to a script by the Cardano ledger when being executed. /// /// The context contains information about the entire transaction that contains @@ -104,10 +107,13 @@ pub type Transaction { withdrawals: Pairs, validity_range: ValidityRange, extra_signatories: List, + /// ⚠️ | Redeemers are ordered by ascending [ScriptPurpose](./governance.html#ScriptPurpose). redeemers: Pairs, datums: Dict, - id: Hash, - votes: Dict>, + id: TransactionId, + /// ⚠️ | Votes are ordered by ascending [Voter](./governance.html#Voter) and [GovernanceActionId](./governance.html#GovernanceActionId).
First constructor variants in a type are treated as lower indices; except for [Credential](./credential.html#Credential) where stake credentials are treated as lower values than verification key credentials. + /// --- | --- + votes: Pairs>, proposal_procedures: List, current_treasury_amount: Option, treasury_donation: Option, @@ -140,7 +146,7 @@ pub fn placeholder() -> Transaction { redeemers: [], datums: dict.new(), id: #"0000000000000000000000000000000000000000000000000000000000000000", - votes: dict.new(), + votes: [], proposal_procedures: [], current_treasury_amount: None, treasury_donation: None, From 5be503c87026d6ec2fd3b9d88c3f781e0bb0a43d Mon Sep 17 00:00:00 2001 From: KtorZ Date: Tue, 13 Aug 2024 23:07:11 +0200 Subject: [PATCH 29/30] Fix withdrawals in Plutus V3 context. --- lib/cardano/transaction.ak | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/cardano/transaction.ak b/lib/cardano/transaction.ak index 9e45e53..9c152f1 100644 --- a/lib/cardano/transaction.ak +++ b/lib/cardano/transaction.ak @@ -8,7 +8,7 @@ use aiken/option use cardano/assets.{Lovelace, PolicyId, Value} use cardano/certificate.{Certificate} use cardano/credential.{ - Address, DatumHash, Script, ScriptHash, StakeCredential, VerificationKey, + Address, Credential, DatumHash, Script, ScriptHash, VerificationKey, VerificationKeyHash, } use cardano/governance.{GovernanceActionId, ProposalProcedure, Vote, Voter} @@ -42,7 +42,7 @@ pub type ScriptInfo { /// For scripts that validate reward withdrawals from a reward account. /// /// The argument identifies the target reward account. - Withdrawing(StakeCredential) + Withdrawing(Credential) /// Needed when delegating to a pool using stake credentials defined as a /// Plutus script. This purpose is also triggered when de-registering such /// stake credentials. @@ -73,7 +73,7 @@ pub type ScriptPurpose { /// For scripts that validate reward withdrawals from a reward account. /// /// The argument identifies the target reward account. - Withdraw(StakeCredential) + Withdraw(Credential) /// Needed when delegating to a pool using stake credentials defined as a /// Plutus script. This purpose is also triggered when de-registering such /// stake credentials. @@ -104,7 +104,9 @@ pub type Transaction { fee: Lovelace, mint: Value, certificates: List, - withdrawals: Pairs, + /// ⚠️ | Withdrawals are ordered by ascending [Credential](./credential.html#Credential). Yet, note that script credentials are treated as lower values than verification key credentials. + /// --- | --- + withdrawals: Pairs, validity_range: ValidityRange, extra_signatories: List, /// ⚠️ | Redeemers are ordered by ascending [ScriptPurpose](./governance.html#ScriptPurpose). From 0d081249aa36698cb1b1fe11d5dfbeed537358eb Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 28 Aug 2024 15:01:45 +0200 Subject: [PATCH 30/30] Review documentation, module organization and missing primitives. --- CHANGELOG.md | 64 + aiken.toml | 2 +- lib/aiken/cbor.ak | 34 +- lib/aiken/collection/dict.ak | 1031 ++++++------ lib/aiken/collection/list.ak | 1420 +++++++++-------- lib/aiken/collection/pairs.ak | 614 +++---- lib/aiken/crypto.ak | 147 ++ lib/aiken/hash.ak | 83 - lib/aiken/interval.ak | 596 +++---- lib/aiken/math.ak | 248 +-- lib/aiken/math/rational.ak | 865 +++++----- lib/aiken/option.ak | 273 ++-- lib/aiken/primitive/bytearray.ak | 671 ++++---- lib/aiken/primitive/int.ak | 78 +- lib/aiken/primitive/string.ak | 57 +- lib/cardano/{credential.ak => address.ak} | 48 +- lib/cardano/assets.ak | 706 ++++---- lib/cardano/certificate.ak | 66 +- lib/cardano/governance.ak | 4 +- lib/cardano/governance/protocol_parameters.ak | 1 - lib/cardano/script_context.ak | 62 + lib/cardano/transaction.ak | 171 +- 22 files changed, 3784 insertions(+), 3457 deletions(-) create mode 100644 lib/aiken/crypto.ak delete mode 100644 lib/aiken/hash.ak rename lib/cardano/{credential.ak => address.ak} (83%) create mode 100644 lib/cardano/script_context.ak diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bec225..bdc4355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,69 @@ # Changelog +## v2.0.0 - UNRELEASED + +### Added + +- New modules covering Conway-related features (i.e. governance) + - [`cardano/governance`](https://aiken-lang.github.io/stdlib/cardano/governance.html) + - [`cardano/governance/protocol_parameters`](https://aiken-lang.github.io/stdlib/cardano/governance/protocol_parameters.html) + +- New primitives in `aiken/crypto`: + - [`blake2b_224`](https://aiken-lang.github.io/stdlib/aiken/crypto.html#blake2b_224) + - [`keccak_256`](https://aiken-lang.github.io/stdlib/aiken/crypto.html#keccak_256) + +- New primitives in `aiken/math`: + - [`log2`](https://aiken-lang.github.io/stdlib/aiken/math.html#log2) + +- New primitives in `aiken/primitive/bytearray`: + - [`at`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#at) + - [`from_int_big_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#from_int_big_endian) + - [`from_int_little_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#from_int_little_endian) + - [`to_int_big_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#to_int_big_endian) + - [`to_int_little_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#to_int_little_endian) + +- New primitives in `aiken/primitive/int`: + - [`from_bytearray_big_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/int.html#from_bytearray_big_endian) + - [`from_bytearray_little_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/int.html#from_bytearray_little_endian) + +- New primitives in `aiken/crypto`: + - [`verify_ecdsa_signature`](https://aiken-lang.github.io/stdlib/cardano/credential.html#verify_ecdsa_signature) + - [`verify_schnorr_signature`](https://aiken-lang.github.io/stdlib/cardano/credential.html#verify_schnorr_signature) + +### Changed + +- Few modules have been relocated and better organized: + - `aiken/hash` -> [`aiken/crypto`](https://aiken-lang.github.io/stdlib/aiken/crypto.html) + - **collections** + - `aiken/dict` -> [`aiken/collection/dict`](https://aiken-lang.github.io/stdlib/aiken/collection/dict.html) + - `aiken/list` -> [`aiken/collection/list`](https://aiken-lang.github.io/stdlib/aiken/collection/list.html) + - `aiken/pairs` -> [`aiken/collection/pairs`](https://aiken-lang.github.io/stdlib/aiken/collection/pairs.html) + - **primitive** + - `aiken/bytearray` -> [`aiken/primitive/bytearray`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html) + - `aiken/int` -> [`aiken/primitive/int`](https://aiken-lang.github.io/stdlib/aiken/primitive/int.html) + - `aiken/string` -> [`aiken/primitive/string`](https://aiken-lang.github.io/stdlib/aiken/primitive/string.html) + - **cardano** + - `aiken/transaction` -> [`cardano/transaction`](https://aiken-lang.github.io/stdlib/cardano/transaction.html) + - `aiken/transaction/certificate` -> [`cardano/certificate`](https://aiken-lang.github.io/stdlib/cardano/certificate.html) + - `aiken/transaction/credential` -> [`cardano/address`](https://aiken-lang.github.io/stdlib/cardano/address.html) & `aiken/crypto` + - `aiken/transaction/value` -> [`cardano/assets`](https://aiken-lang.github.io/stdlib/cardano/assets.html) + +- The `Transaction` type from [`cardano/transaction`](https://aiken-lang.github.io/stdlib/cardano/transaction.html) (originally `aiken/transaction`) has been greatly reworked to match the new transaction format in Plutus V3. + +- The `ScriptContext` type has split from `cardano/transaction` (originally `aiken/transaction`) and moved into its own module [`cardano/script_context`](https://aiken-lang.github.io/stdlib/cardano/script_context.html) and adjusted to its new form as per Plutus V3. + +- The constructors of [`Credential`](https://aiken-lang.github.io/stdlib/cardano/address.html#credential) have been renamed from `VerificationKeyCredential` and `ScriptCredential` into `VerificationKey` and `Script` respectively. + +- The function `remove_all`, `remove_first` and `remove_last` from [`aiken/collection/pairs`](https://aiken-lang.github.io/stdlib/aiken/collection/pairs.html) (originally `aiken/pairs`) have been renamed to `delete_all`, `delete_first` and `delete_last` respectively. + +- The function `verify_signature` from [`aiken/crypto`](https://aiken-lang.github.io/stdlib/aiken/crypto.html) (originally `aiken/credential`) has been renamed to `verify_ed25519_signature`. + +### Removed + +- The module `aiken/time`. The `PosixTime` alias is no longer used anywhere. + +- `MintedValue` (from `aiken/transaction/value` originally) and its associated functions are no longer needed and, therefore, gone. + ## v1.9.0 - 2024-05-24 ### Added diff --git a/aiken.toml b/aiken.toml index 5d5dc77..d9881e5 100644 --- a/aiken.toml +++ b/aiken.toml @@ -1,5 +1,5 @@ name = "aiken-lang/stdlib" -version = "1.9.0" +version = "main" licences = ["Apache-2.0"] description = "The Aiken Standard Library" diff --git a/lib/aiken/cbor.ak b/lib/aiken/cbor.ak index e414edb..64de402 100644 --- a/lib/aiken/cbor.ak +++ b/lib/aiken/cbor.ak @@ -13,14 +13,14 @@ use aiken/builtin.{decode_utf8, serialise_data} /// useful for debugging. /// /// ```aiken -/// serialise(42) == #"182a" -/// serialise(#"a1b2") == #"42a1b2" -/// serialise([]) == #"80" -/// serialise((1, 2)) == #"9f0102ff" -/// serialise((1, #"ff", 3)) == #"9f0141ff03ff" -/// serialise([(1, #"ff")]) == #"a10141ff" -/// serialise(Some(42)) == #"d8799f182aff" -/// serialise(None) == #"d87a80" +/// cbor.serialise(42) == #"182a" +/// cbor.serialise(#"a1b2") == #"42a1b2" +/// cbor.serialise([]) == #"80" +/// cbor.serialise((1, 2)) == #"9f0102ff" +/// cbor.serialise((1, #"ff", 3)) == #"9f0141ff03ff" +/// cbor.serialise([(1, #"ff")]) == #"a10141ff" +/// cbor.serialise(Some(42)) == #"d8799f182aff" +/// cbor.serialise(None) == #"d87a80" /// ``` pub fn serialise(self: Data) -> ByteArray { serialise_data(self) @@ -73,15 +73,15 @@ test serialise_9() { /// a good idea in the Cardano world. /// /// ```aiken -/// diagnostic(42) == "42" -/// diagnostic(#"a1b2") == "h'A1B2'" -/// diagnostic([1, 2, 3]) == "[_ 1, 2, 3]" -/// diagnostic([]) == "[]" -/// diagnostic((1, 2)) == "[_ 1, 2]" -/// diagnostic((1, #"ff", 3)) == "[_ 1, h'FF', 3]" -/// diagnostic([(1, #"ff")]) == "{_ 1: h'FF' }" -/// diagnostic(Some(42)) == "121([_ 42])" -/// diagnostic(None) == "122([])" +/// cbor.diagnostic(42) == "42" +/// cbor.diagnostic(#"a1b2") == "h'A1B2'" +/// cbor.diagnostic([1, 2, 3]) == "[_ 1, 2, 3]" +/// cbor.diagnostic([]) == "[]" +/// cbor.diagnostic((1, 2)) == "[_ 1, 2]" +/// cbor.diagnostic((1, #"ff", 3)) == "[_ 1, h'FF', 3]" +/// cbor.diagnostic([(1, #"ff")]) == "{_ 1: h'FF' }" +/// cbor.diagnostic(Some(42)) == "121([_ 42])" +/// cbor.diagnostic(None) == "122([])" /// ``` pub fn diagnostic(self: Data) -> String { aiken.diagnostic(self, #"") diff --git a/lib/aiken/collection/dict.ak b/lib/aiken/collection/dict.ak index d280443..c9f167f 100644 --- a/lib/aiken/collection/dict.ak +++ b/lib/aiken/collection/dict.ak @@ -1,15 +1,16 @@ //// A module for working with bytearray dictionaries. //// -//// ### Important //// -//// Dictionaries are **ordered sets** of key-value pairs, which thus -//// preserve some invariants. Specifically, each key is only present once in -//// the dictionary and all keys are stored in ascending lexicographic order. +//// > [!IMPORTANT] +//// > +//// > Dictionaries are **ordered sets** of key-value pairs, which thus +//// > preserve some invariants. Specifically, each key is only present once in +//// > the dictionary and all keys are stored in ascending lexicographic order. //// -//// These invariants allow for more optimized functions to operate on `Dict`, -//// but as a trade-offs, prevent `Dict` from being serializable. To recover a `Dict` -//// from an unknown `Data`, you must first recover an `Pairs` and use -//// `dict.from_ascending_list`. +//// > These invariants allow for more optimized functions to operate on `Dict`, +//// > but as a trade-offs, prevent `Dict` from being serializable. To recover a `Dict` +//// > from an unknown `Data`, you must first recover an `Pairs` and use +//// > [`dict.from_ascending_list`](#from_ascending_list). use aiken/builtin @@ -30,6 +31,8 @@ pub opaque type Dict { inner: Pairs, } +// ## Constructing + /// Create a new empty Dict /// ```aiken /// dict.to_pairs(dict.new()) == [] @@ -50,147 +53,179 @@ fn fixture_1() { |> insert(bar, 14) } -/// Remove a key-value pair from the dictionary. If the key is not found, no changes are made. +/// Like ['from_pairs'](#from_pairs), but from an already sorted list by ascending +/// keys. This function fails (i.e. halt the program execution) if the list isn't +/// sorted. /// /// ```aiken +/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// /// let result = -/// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.delete(key: "a") +/// dict.from_ascending_pairs(pairs) /// |> dict.to_pairs() /// -/// result == [Pair("b", 200)] +/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] /// ``` -pub fn delete(self: Dict, key: ByteArray) -> Dict { - Dict { inner: do_delete(self.inner, key) } +/// +/// This is meant to be used to turn a list constructed off-chain into a `Dict` +/// which has taken care of maintaining interval invariants. This function still +/// performs a sanity check on all keys to avoid silly mistakes. It is, however, +/// considerably faster than ['from_pairs'](from_pairs) +pub fn from_ascending_pairs(xs: Pairs) -> Dict { + let Void = check_ascending_list(xs) + Dict { inner: xs } } -fn do_delete( - self: Pairs, - key k: ByteArray, -) -> Pairs { - when self is { - [] -> - [] - [Pair(k2, v2), ..rest] -> - if builtin.less_than_equals_bytearray(k, k2) { - if k == k2 { - rest - } else { - self - } +fn check_ascending_list(xs: Pairs) { + when xs is { + [] -> Void + [_] -> Void + [Pair(x0, _), Pair(x1, _) as e, ..rest] -> + if builtin.less_than_bytearray(x0, x1) { + check_ascending_list([e, ..rest]) } else { - [Pair(k2, v2), ..do_delete(rest, k)] + fail @"keys in associative list aren't in ascending order" } } } -test delete_1() { - delete(new(), foo) == new() -} - -test delete_2() { - let m = - new() - |> insert(foo, 14) - delete(m, foo) == new() -} - -test delete_3() { - let m = - new() - |> insert(foo, 14) - delete(m, bar) == m -} - -test delete_4() { - let m = - new() - |> insert(foo, 14) - |> insert(bar, 14) - !has_key(delete(m, foo), foo) +/// Like [`from_ascending_pairs`](#from_ascending_pairs) but fails if **any** +/// value doesn't satisfy the predicate. +/// +/// ```aiken +/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// +/// dict.from_ascending_pairs_with(pairs, fn(x) { x <= 250 }) // fail +/// ``` +pub fn from_ascending_pairs_with( + xs: Pairs, + predicate: fn(value) -> Bool, +) -> Dict { + let Void = check_ascending_pairs_with(xs, predicate) + Dict { inner: xs } } -test delete_5() { - let m = - new() - |> insert(foo, 14) - |> insert(bar, 14) - has_key(delete(m, bar), foo) +fn check_ascending_pairs_with( + xs: Pairs, + predicate: fn(value) -> Bool, +) { + when xs is { + [] -> Void + [Pair(_, v)] -> + if predicate(v) { + Void + } else { + fail @"value doesn't satisfy predicate" + } + [Pair(x0, v0), Pair(x1, _) as e, ..rest] -> + if builtin.less_than_bytearray(x0, x1) { + if predicate(v0) { + check_ascending_pairs_with([e, ..rest], predicate) + } else { + fail @"value doesn't satisfy predicate" + } + } else { + fail @"keys in pairs aren't in ascending order" + } + } } -test delete_6() { - let m = - new() - |> insert("aaa", 1) - |> insert("bbb", 2) - |> insert("ccc", 3) - |> insert("ddd", 4) - |> insert("eee", 5) - |> insert("fff", 6) - |> insert("ggg", 7) - |> insert("hhh", 8) - |> insert("iii", 9) - |> insert("jjj", 10) +test bench_from_ascending_pairs() { + let dict = + from_ascending_pairs( + [ + Pair("aaaa", 1), + Pair("aaab", 9), + Pair("aaba", 5), + Pair("aabb", 13), + Pair("abaa", 2), + Pair("abab", 10), + Pair("abba", 6), + Pair("abbb", 14), + Pair("baaa", 3), + Pair("baab", 11), + Pair("baba", 7), + Pair("babb", 15), + Pair("bbaa", 4), + Pair("bbab", 12), + Pair("bbba", 8), + Pair("bbbb", 16), + ], + ) - delete(m, "bcd") == m + size(dict) == 16 } -/// Keep only the key-value pairs that pass the given predicate. +/// Construct a dictionary from a list of key-value pairs. Note that when a key is present +/// multiple times, the first occurrence prevails. /// /// ```aiken +/// let pairs = [Pair("a", 100), Pair("c", 300), Pair("b", 200)] +/// /// let result = -/// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.insert(key: "c", value: 300) -/// |> dict.filter(fn(k, _v) { k != "a" }) +/// dict.from_pairs(pairs) /// |> dict.to_pairs() /// -/// result == [Pair("b", 200), Pair("c", 300)] +/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] /// ``` -pub fn filter( - self: Dict, - with: fn(ByteArray, value) -> Bool, -) -> Dict { - Dict { inner: do_filter(self.inner, with) } +pub fn from_pairs(self: Pairs) -> Dict { + Dict { inner: do_from_pairs(self) } } -fn do_filter( - self: Pairs, - with: fn(ByteArray, value) -> Bool, -) -> Pairs { - when self is { +fn do_from_pairs(xs: Pairs) -> Pairs { + when xs is { [] -> [] - [Pair(k, v), ..rest] -> - if with(k, v) { - [Pair(k, v), ..do_filter(rest, with)] - } else { - do_filter(rest, with) - } + [Pair(k, v), ..rest] -> do_insert(do_from_pairs(rest), k, v) } } -test filter_1() { - filter(new(), fn(_, _) { True }) == new() +test from_list_1() { + from_pairs([]) == new() } -test filter_2() { - let expected = - new() - |> insert(foo, 42) - filter(fixture_1(), fn(_, v) { v > 14 }) == expected +test from_list_2() { + from_pairs([Pair(foo, 42), Pair(bar, 14)]) == from_pairs( + [Pair(bar, 14), Pair(foo, 42)], + ) } -test filter_3() { - let expected = - new() - |> insert(bar, 14) - filter(fixture_1(), fn(k, _) { k == bar }) == expected +test from_list_3() { + from_pairs([Pair(foo, 42), Pair(bar, 14)]) == fixture_1() +} + +test from_list_4() { + from_pairs([Pair(foo, 42), Pair(bar, 14), Pair(foo, 1337)]) == fixture_1() +} + +test bench_from_pairs() { + let dict = + from_pairs( + [ + Pair("bbba", 8), + Pair("bbab", 12), + Pair("aabb", 13), + Pair("aaab", 9), + Pair("bbbb", 16), + Pair("aaaa", 1), + Pair("aaba", 5), + Pair("abab", 10), + Pair("baba", 7), + Pair("baab", 11), + Pair("abaa", 2), + Pair("baaa", 3), + Pair("bbaa", 4), + Pair("babb", 15), + Pair("abbb", 14), + Pair("abba", 6), + ], + ) + + size(dict) == 16 } +// ## Inspecting + /// Finds a value in the dictionary, and returns the first key found to have that value. /// /// ```aiken @@ -249,394 +284,417 @@ test find_4() { ) == Some(baz) } -/// Fold over the key-value pairs in a dictionary. The fold direction follows keys -/// in ascending order and is done from right-to-left. +/// Get a value in the dict by its key. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.insert(key: "c", value: 300) -/// |> dict.foldr(0, fn(_k, v, r) { v + r }) +/// |> dict.insert(key: "a", value: "Aiken") +/// |> dict.get(key: "a") /// -/// result == 600 +/// result == Some("Aiken") /// ``` -pub fn foldr( - self: Dict, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { - do_foldr(self.inner, zero, with) +pub fn get(self: Dict, key: ByteArray) -> Option { + do_get(self.inner, key) } -fn do_foldr( - self: Pairs, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { +fn do_get(self: Pairs, key k: ByteArray) -> Option { when self is { - [] -> zero - [Pair(k, v), ..rest] -> with(k, v, do_foldr(rest, zero, with)) + [] -> None + [Pair(k2, v), ..rest] -> + if builtin.less_than_equals_bytearray(k, k2) { + if k == k2 { + Some(v) + } else { + None + } + } else { + do_get(rest, k) + } } } -test foldr_1() { - foldr(new(), 14, fn(_, _, _) { 42 }) == 14 +test get_1() { + get(new(), foo) == None } -test foldr_2() { - foldr(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +test get_2() { + let m = + new() + |> insert(foo, "Aiken") + |> insert(bar, "awesome") + get(m, key: foo) == Some("Aiken") } -/// Fold over the key-value pairs in a dictionary. The fold direction follows keys -/// in ascending order and is done from left-to-right. +test get_3() { + let m = + new() + |> insert(foo, "Aiken") + |> insert(bar, "awesome") + get(m, key: baz) == None +} + +test get_4() { + let m = + new() + |> insert("aaa", "1") + |> insert("bbb", "2") + |> insert("ccc", "3") + |> insert("ddd", "4") + |> insert("eee", "5") + |> insert("fff", "6") + |> insert("ggg", "7") + |> insert("hhh", "8") + |> insert("iii", "9") + |> insert("jjj", "10") + + get(m, "bcd") == None +} + +test get_5() { + let m = + new() + |> insert("aaa", "1") + |> insert("bbb", "2") + |> insert("ccc", "3") + |> insert("ddd", "4") + |> insert("eee", "5") + |> insert("fff", "6") + |> insert("ggg", "7") + |> insert("hhh", "8") + |> insert("iii", "9") + |> insert("jjj", "10") + + get(m, "kkk") == None +} + +/// Check if a key exists in the dictionary. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.insert(key: "c", value: 300) -/// |> dict.foldl(0, fn(_k, v, r) { v + r }) +/// |> dict.insert(key: "a", value: "Aiken") +/// |> dict.has_key("a") /// -/// result == 600 +/// result == True /// ``` -pub fn foldl( - self: Dict, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { - do_foldl(self.inner, zero, with) +pub fn has_key(self: Dict, key k: ByteArray) -> Bool { + do_has_key(self.inner, k) } -fn do_foldl( - self: Pairs, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { +fn do_has_key(self: Pairs, key k: ByteArray) -> Bool { when self is { - [] -> zero - [Pair(k, v), ..rest] -> do_foldl(rest, with(k, v, zero), with) + [] -> False + [Pair(k2, _), ..rest] -> + if builtin.less_than_equals_bytearray(k, k2) { + k == k2 + } else { + do_has_key(rest, k) + } } } -test fold_1() { - foldl(new(), 14, fn(_, _, _) { 42 }) == 14 +test has_key_1() { + !has_key(new(), foo) } -test fold_2() { - foldl(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +test has_key_2() { + has_key( + new() + |> insert(foo, 14), + foo, + ) } -/// Construct a dictionary from a list of key-value pairs. Note that when a key is present -/// multiple times, the first occurrence prevails. -/// +test has_key_3() { + !has_key( + new() + |> insert(foo, 14), + bar, + ) +} + +test has_key_4() { + has_key( + new() + |> insert(foo, 14) + |> insert(bar, 42), + bar, + ) +} + +/// Efficiently checks whether a dictionary is empty. /// ```aiken -/// let pairs = [Pair("a", 100), Pair("c", 300), Pair("b", 200)] +/// dict.is_empty(dict.new()) == True +/// ``` +pub fn is_empty(self: Dict) -> Bool { + when self.inner is { + [] -> True + _ -> False + } +} + +test is_empty_1() { + is_empty(new()) +} + +/// Extract all the keys present in a given `Dict`. /// +/// ```aiken /// let result = -/// dict.from_pairs(pairs) -/// |> dict.to_pairs() +/// dict.new() +/// |> dict.insert("a", 14) +/// |> dict.insert("b", 42) +/// |> dict.insert("a", 1337) +/// |> dict.keys() /// -/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// result == ["a", "b"] /// ``` -pub fn from_pairs(self: Pairs) -> Dict { - Dict { inner: do_from_pairs(self) } +pub fn keys(self: Dict) -> List { + do_keys(self.inner) } -fn do_from_pairs(xs: Pairs) -> Pairs { - when xs is { +fn do_keys(self: Pairs) -> List { + when self is { [] -> [] - [Pair(k, v), ..rest] -> do_insert(do_from_pairs(rest), k, v) + [Pair(k, _), ..rest] -> + [k, ..do_keys(rest)] } } -test from_list_1() { - from_pairs([]) == new() -} - -test from_list_2() { - from_pairs([Pair(foo, 42), Pair(bar, 14)]) == from_pairs( - [Pair(bar, 14), Pair(foo, 42)], - ) -} - -test from_list_3() { - from_pairs([Pair(foo, 42), Pair(bar, 14)]) == fixture_1() -} - -test from_list_4() { - from_pairs([Pair(foo, 42), Pair(bar, 14), Pair(foo, 1337)]) == fixture_1() +test keys_1() { + keys(new()) == [] } -test bench_from_pairs() { - let dict = - from_pairs( - [ - Pair("bbba", 8), - Pair("bbab", 12), - Pair("aabb", 13), - Pair("aaab", 9), - Pair("bbbb", 16), - Pair("aaaa", 1), - Pair("aaba", 5), - Pair("abab", 10), - Pair("baba", 7), - Pair("baab", 11), - Pair("abaa", 2), - Pair("baaa", 3), - Pair("bbaa", 4), - Pair("babb", 15), - Pair("abbb", 14), - Pair("abba", 6), - ], - ) - - size(dict) == 16 +test keys_2() { + keys( + new() + |> insert(foo, 0) + |> insert(bar, 0), + ) == [bar, foo] } -/// Like ['from_list'](from_list), but from an already sorted list by ascending -/// keys. This function fails (i.e. halt the program execution) if the list isn't -/// sorted. +/// Return the number of key-value pairs in the dictionary. /// /// ```aiken -/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] -/// /// let result = -/// dict.from_ascending_pairs(pairs) -/// |> dict.to_pairs() +/// dict.new() +/// |> dict.insert("a", 100) +/// |> dict.insert("b", 200) +/// |> dict.insert("c", 300) +/// |> dict.size() /// -/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// result == 3 /// ``` -/// -/// This is meant to be used to turn a list constructed off-chain into a `Dict` -/// which has taken care of maintaining interval invariants. This function still -/// performs a sanity check on all keys to avoid silly mistakes. It is, however, -/// considerably faster than ['from_list'](from_list) -pub fn from_ascending_pairs(xs: Pairs) -> Dict { - let Void = check_ascending_list(xs) - Dict { inner: xs } +pub fn size(self: Dict) -> Int { + do_size(self.inner) } -fn check_ascending_list(xs: Pairs) { - when xs is { - [] -> Void - [_] -> Void - [Pair(x0, _), Pair(x1, _) as e, ..rest] -> - if builtin.less_than_bytearray(x0, x1) { - check_ascending_list([e, ..rest]) - } else { - fail @"keys in associative list aren't in ascending order" - } +fn do_size(self: Pairs) -> Int { + when self is { + [] -> 0 + [_, ..rest] -> 1 + do_size(rest) } } -/// Like [`from_ascending_pairs`](#from_ascending_list) but fails if **any** -/// value doesn't satisfy the predicate. +test size_1() { + size(new()) == 0 +} + +test size_2() { + size( + new() + |> insert(foo, 14), + ) == 1 +} + +test size_3() { + size( + new() + |> insert(foo, 14) + |> insert(bar, 42), + ) == 2 +} + +/// Extract all the values present in a given `Dict`. /// /// ```aiken -/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// let result = +/// dict.new() +/// |> dict.insert("a", 14) +/// |> dict.insert("b", 42) +/// |> dict.insert("c", 1337) +/// |> dict.values() /// -/// dict.from_ascending_pairs_with(pairs, fn(x) { x <= 250 }) // fail +/// result == [1337, 42] /// ``` -pub fn from_ascending_pairs_with( - xs: Pairs, - predicate: fn(value) -> Bool, -) -> Dict { - let Void = check_ascending_pairs_with(xs, predicate) - Dict { inner: xs } +pub fn values(self: Dict) -> List { + do_values(self.inner) } -fn check_ascending_pairs_with( - xs: Pairs, - predicate: fn(value) -> Bool, -) { - when xs is { - [] -> Void - [Pair(_, v)] -> - if predicate(v) { - Void - } else { - fail @"value doesn't satisfy predicate" - } - [Pair(x0, v0), Pair(x1, _) as e, ..rest] -> - if builtin.less_than_bytearray(x0, x1) { - if predicate(v0) { - check_ascending_pairs_with([e, ..rest], predicate) - } else { - fail @"value doesn't satisfy predicate" - } - } else { - fail @"keys in pairs aren't in ascending order" - } +fn do_values(self: Pairs) -> List { + when self is { + [] -> + [] + [Pair(_, v), ..rest] -> + [v, ..do_values(rest)] } } -test bench_from_ascending_pairs() { - let dict = - from_ascending_pairs( - [ - Pair("aaaa", 1), - Pair("aaab", 9), - Pair("aaba", 5), - Pair("aabb", 13), - Pair("abaa", 2), - Pair("abab", 10), - Pair("abba", 6), - Pair("abbb", 14), - Pair("baaa", 3), - Pair("baab", 11), - Pair("baba", 7), - Pair("babb", 15), - Pair("bbaa", 4), - Pair("bbab", 12), - Pair("bbba", 8), - Pair("bbbb", 16), - ], - ) +test values_1() { + values(new()) == [] +} - size(dict) == 16 +test values_2() { + values( + new() + |> insert(foo, 3) + |> insert(bar, 4), + ) == [4, 3] } -/// Get a value in the dict by its key. +// ## Modifying + +/// Remove a key-value pair from the dictionary. If the key is not found, no changes are made. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: "Aiken") -/// |> dict.get(key: "a") +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.delete(key: "a") +/// |> dict.to_pairs() /// -/// result == Some("Aiken") +/// result == [Pair("b", 200)] /// ``` -pub fn get(self: Dict, key: ByteArray) -> Option { - do_get(self.inner, key) +pub fn delete(self: Dict, key: ByteArray) -> Dict { + Dict { inner: do_delete(self.inner, key) } } -fn do_get(self: Pairs, key k: ByteArray) -> Option { +fn do_delete( + self: Pairs, + key k: ByteArray, +) -> Pairs { when self is { - [] -> None - [Pair(k2, v), ..rest] -> + [] -> + [] + [Pair(k2, v2), ..rest] -> if builtin.less_than_equals_bytearray(k, k2) { if k == k2 { - Some(v) + rest } else { - None + self } } else { - do_get(rest, k) + [Pair(k2, v2), ..do_delete(rest, k)] } } } -test get_1() { - get(new(), foo) == None +test delete_1() { + delete(new(), foo) == new() } -test get_2() { +test delete_2() { let m = new() - |> insert(foo, "Aiken") - |> insert(bar, "awesome") - get(m, key: foo) == Some("Aiken") + |> insert(foo, 14) + delete(m, foo) == new() } -test get_3() { +test delete_3() { let m = new() - |> insert(foo, "Aiken") - |> insert(bar, "awesome") - get(m, key: baz) == None + |> insert(foo, 14) + delete(m, bar) == m } -test get_4() { +test delete_4() { let m = new() - |> insert("aaa", "1") - |> insert("bbb", "2") - |> insert("ccc", "3") - |> insert("ddd", "4") - |> insert("eee", "5") - |> insert("fff", "6") - |> insert("ggg", "7") - |> insert("hhh", "8") - |> insert("iii", "9") - |> insert("jjj", "10") + |> insert(foo, 14) + |> insert(bar, 14) + !has_key(delete(m, foo), foo) +} - get(m, "bcd") == None +test delete_5() { + let m = + new() + |> insert(foo, 14) + |> insert(bar, 14) + has_key(delete(m, bar), foo) } -test get_5() { +test delete_6() { let m = new() - |> insert("aaa", "1") - |> insert("bbb", "2") - |> insert("ccc", "3") - |> insert("ddd", "4") - |> insert("eee", "5") - |> insert("fff", "6") - |> insert("ggg", "7") - |> insert("hhh", "8") - |> insert("iii", "9") - |> insert("jjj", "10") + |> insert("aaa", 1) + |> insert("bbb", 2) + |> insert("ccc", 3) + |> insert("ddd", 4) + |> insert("eee", 5) + |> insert("fff", 6) + |> insert("ggg", 7) + |> insert("hhh", 8) + |> insert("iii", 9) + |> insert("jjj", 10) - get(m, "kkk") == None + delete(m, "bcd") == m } -/// Check if a key exists in the dictionary. +/// Keep only the key-value pairs that pass the given predicate. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: "Aiken") -/// |> dict.has_key("a") +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.insert(key: "c", value: 300) +/// |> dict.filter(fn(k, _v) { k != "a" }) +/// |> dict.to_pairs() /// -/// result == True +/// result == [Pair("b", 200), Pair("c", 300)] /// ``` -pub fn has_key(self: Dict, key k: ByteArray) -> Bool { - do_has_key(self.inner, k) +pub fn filter( + self: Dict, + with: fn(ByteArray, value) -> Bool, +) -> Dict { + Dict { inner: do_filter(self.inner, with) } } -fn do_has_key(self: Pairs, key k: ByteArray) -> Bool { +fn do_filter( + self: Pairs, + with: fn(ByteArray, value) -> Bool, +) -> Pairs { when self is { - [] -> False - [Pair(k2, _), ..rest] -> - if builtin.less_than_equals_bytearray(k, k2) { - k == k2 + [] -> + [] + [Pair(k, v), ..rest] -> + if with(k, v) { + [Pair(k, v), ..do_filter(rest, with)] } else { - do_has_key(rest, k) - } - } -} - -test has_key_1() { - !has_key(new(), foo) + do_filter(rest, with) + } + } } -test has_key_2() { - has_key( - new() - |> insert(foo, 14), - foo, - ) +test filter_1() { + filter(new(), fn(_, _) { True }) == new() } -test has_key_3() { - !has_key( +test filter_2() { + let expected = new() - |> insert(foo, 14), - bar, - ) + |> insert(foo, 42) + filter(fixture_1(), fn(_, v) { v > 14 }) == expected } -test has_key_4() { - has_key( +test filter_3() { + let expected = new() - |> insert(foo, 14) - |> insert(bar, 42), - bar, - ) + |> insert(bar, 14) + filter(fixture_1(), fn(k, _) { k == bar }) == expected } /// Insert a value in the dictionary at a given key. If the key already exists, its value is **overridden**. If you need ways to combine keys together, use (`insert_with`)[#insert_with]. @@ -776,58 +834,6 @@ test insert_with_3() { result == [Pair("foo", 1)] } -/// Efficiently checks whether a dictionary is empty. -/// ```aiken -/// dict.is_empty(dict.new()) == True -/// ``` -pub fn is_empty(self: Dict) -> Bool { - when self.inner is { - [] -> True - _ -> False - } -} - -test is_empty_1() { - is_empty(new()) -} - -/// Extract all the keys present in a given `Dict`. -/// -/// ```aiken -/// let result = -/// dict.new() -/// |> dict.insert("a", 14) -/// |> dict.insert("b", 42) -/// |> dict.insert("a", 1337) -/// |> dict.keys() -/// -/// result == ["a", "b"] -/// ``` -pub fn keys(self: Dict) -> List { - do_keys(self.inner) -} - -fn do_keys(self: Pairs) -> List { - when self is { - [] -> - [] - [Pair(k, _), ..rest] -> - [k, ..do_keys(rest)] - } -} - -test keys_1() { - keys(new()) == [] -} - -test keys_2() { - keys( - new() - |> insert(foo, 0) - |> insert(bar, 0), - ) == [bar, foo] -} - /// Apply a function to all key-value pairs in a Dict. /// /// ```aiken @@ -871,71 +877,7 @@ test map_2() { get(result, foo) == Some(43) && size(result) == size(fixture_1()) } -/// Get the inner list holding the dictionary data. -/// -/// ```aiken -/// let result = -/// dict.new() -/// |> dict.insert("a", 100) -/// |> dict.insert("b", 200) -/// |> dict.insert("c", 300) -/// |> dict.to_pairs() -/// -/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] -/// ``` -pub fn to_pairs(self: Dict) -> Pairs { - self.inner -} - -test to_list_1() { - to_pairs(new()) == [] -} - -test to_list_2() { - to_pairs(fixture_1()) == [Pair(bar, 14), Pair(foo, 42)] -} - -/// Return the number of key-value pairs in the dictionary. -/// -/// ```aiken -/// let result = -/// dict.new() -/// |> dict.insert("a", 100) -/// |> dict.insert("b", 200) -/// |> dict.insert("c", 300) -/// |> dict.size() -/// -/// result == 3 -/// ``` -pub fn size(self: Dict) -> Int { - do_size(self.inner) -} - -fn do_size(self: Pairs) -> Int { - when self is { - [] -> 0 - [_, ..rest] -> 1 + do_size(rest) - } -} - -test size_1() { - size(new()) == 0 -} - -test size_2() { - size( - new() - |> insert(foo, 14), - ) == 1 -} - -test size_3() { - size( - new() - |> insert(foo, 14) - |> insert(bar, 42), - ) == 2 -} +// ## Combining /// Combine two dictionaries. If the same key exist in both the left and /// right dictionary, values from the left are preferred (i.e. left-biaised). @@ -1079,39 +1021,108 @@ test union_with_1() { result == from_pairs([Pair(foo, 1351), Pair(bar, 42)]) } -/// Extract all the values present in a given `Dict`. +// ## Transforming + +/// Fold over the key-value pairs in a dictionary. The fold direction follows keys +/// in ascending order and is done from left-to-right. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert("a", 14) -/// |> dict.insert("b", 42) -/// |> dict.insert("c", 1337) -/// |> dict.values() +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.insert(key: "c", value: 300) +/// |> dict.foldl(0, fn(_k, v, r) { v + r }) /// -/// result == [1337, 42] +/// result == 600 /// ``` -pub fn values(self: Dict) -> List { - do_values(self.inner) +pub fn foldl( + self: Dict, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { + do_foldl(self.inner, zero, with) } -fn do_values(self: Pairs) -> List { +fn do_foldl( + self: Pairs, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { when self is { - [] -> - [] - [Pair(_, v), ..rest] -> - [v, ..do_values(rest)] + [] -> zero + [Pair(k, v), ..rest] -> do_foldl(rest, with(k, v, zero), with) } } -test values_1() { - values(new()) == [] +test fold_1() { + foldl(new(), 14, fn(_, _, _) { 42 }) == 14 } -test values_2() { - values( - new() - |> insert(foo, 3) - |> insert(bar, 4), - ) == [4, 3] +test fold_2() { + foldl(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +} + +/// Fold over the key-value pairs in a dictionary. The fold direction follows keys +/// in ascending order and is done from right-to-left. +/// +/// ```aiken +/// let result = +/// dict.new() +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.insert(key: "c", value: 300) +/// |> dict.foldr(0, fn(_k, v, r) { v + r }) +/// +/// result == 600 +/// ``` +pub fn foldr( + self: Dict, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { + do_foldr(self.inner, zero, with) +} + +fn do_foldr( + self: Pairs, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { + when self is { + [] -> zero + [Pair(k, v), ..rest] -> with(k, v, do_foldr(rest, zero, with)) + } +} + +test foldr_1() { + foldr(new(), 14, fn(_, _, _) { 42 }) == 14 +} + +test foldr_2() { + foldr(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +} + +/// Get the inner list holding the dictionary data. +/// +/// ```aiken +/// let result = +/// dict.new() +/// |> dict.insert("a", 100) +/// |> dict.insert("b", 200) +/// |> dict.insert("c", 300) +/// |> dict.to_pairs() +/// +/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// ``` +pub fn to_pairs(self: Dict) -> Pairs { + self.inner +} + +test to_list_1() { + to_pairs(new()) == [] +} + +test to_list_2() { + to_pairs(fixture_1()) == [Pair(bar, 14), Pair(foo, 42)] } diff --git a/lib/aiken/collection/list.ak b/lib/aiken/collection/list.ak index 3745c49..4fb1319 100644 --- a/lib/aiken/collection/list.ak +++ b/lib/aiken/collection/list.ak @@ -2,6 +2,71 @@ use aiken/builtin use aiken/primitive/bytearray use aiken/primitive/int +// ## Constructing + +/// Add an element in front of the list. Sometimes useful when combined with +/// other functions. +/// +/// ```aiken +/// list.push([2, 3], 1) == [1, ..[2, 3]] == [1, 2, 3] +/// ``` +pub fn push(self: List
, elem: a) -> List { + [elem, ..self] +} + +test push_1() { + push([], 0) == [0] +} + +test push_2() { + push([2, 3], 1) == [1, 2, 3] +} + +/// Construct a list of a integer from a given range. +/// +/// ```aiken +/// list.range(0, 3) == [0, 1, 2, 3] +/// list.range(-1, 1) == [-1, 0, 1] +/// ``` +pub fn range(from: Int, to: Int) -> List { + if from > to { + [] + } else { + [from, ..range(from + 1, to)] + } +} + +test range_1() { + range(0, 3) == [0, 1, 2, 3] +} + +test range_2() { + range(-1, 1) == [-1, 0, 1] +} + +/// Construct a list filled with n copies of a value. +/// +/// ```aiken +/// list.repeat("na", 3) == ["na", "na", "na"] +/// ``` +pub fn repeat(elem: a, n_times: Int) -> List { + if n_times <= 0 { + [] + } else { + [elem, ..repeat(elem, n_times - 1)] + } +} + +test repeat_1() { + repeat(42, 0) == [] +} + +test repeat_2() { + repeat(14, 3) == [14, 14, 14] +} + +// ## Inspecting + /// Determine if all elements of the list satisfy the given predicate. /// /// Note: an empty list always satisfies the predicate. @@ -59,6 +124,44 @@ test any_3() { any([], fn(n) { n == 42 }) == False } +/// Return Some(item) at the index or None if the index is out of range. The index is 0-based. +/// +/// ```aiken +/// list.at([1, 2, 3], 1) == Some(2) +/// list.at([1, 2, 3], 42) == None +/// ``` +pub fn at(self: List, index: Int) -> Option { + when self is { + [] -> None + [x, ..xs] -> + if index == 0 { + Some(x) + } else { + at(xs, index - 1) + } + } +} + +test at_1() { + at([1, 2, 3], -1) == None +} + +test at_2() { + at([], 0) == None +} + +test at_3() { + at([1, 2, 3], 3) == None +} + +test at_4() { + at([1], 0) == Some(1) +} + +test at_5() { + at([1, 2, 3], 2) == Some(3) +} + /// Count how many items in the list satisfy the given predicate. /// /// ```aiken @@ -97,134 +200,233 @@ test count_none() { count([1, 2, 3], fn(a) { a > 5 }) == 0 } -/// Return Some(item) at the index or None if the index is out of range. The index is 0-based. +/// Find the first element satisfying the given predicate, if any. /// /// ```aiken -/// list.at([1, 2, 3], 1) == Some(2) -/// list.at([1, 2, 3], 42) == None +/// list.find([1, 2, 3], fn(x) { x == 2 }) == Some(2) +/// list.find([4, 5, 6], fn(x) { x == 2 }) == None /// ``` -pub fn at(self: List, index: Int) -> Option { +pub fn find(self: List, predicate: fn(a) -> Bool) -> Option { when self is { [] -> None [x, ..xs] -> - if index == 0 { + if predicate(x) { Some(x) } else { - at(xs, index - 1) + find(xs, predicate) } } } -test at_1() { - at([1, 2, 3], -1) == None -} - -test at_2() { - at([], 0) == None -} - -test at_3() { - at([1, 2, 3], 3) == None +test find_1() { + find([1, 2, 3], fn(x) { x == 1 }) == Some(1) } -test at_4() { - at([1], 0) == Some(1) +test find_2() { + find([1, 2, 3], fn(x) { x > 42 }) == None } -test at_5() { - at([1, 2, 3], 2) == Some(3) +test find_3() { + find([], fn(_) { True }) == None } -/// Merge two lists together. +/// Figures out whether a list contain the given element. /// /// ```aiken -/// list.concat([], []) == [] -/// list.concat([], [1, 2, 3]) == [1, 2, 3] -/// list.concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +/// list.has([1, 2, 3], 2) == True +/// list.has([1, 2, 3], 14) == False +/// list.has([], 14) == False /// ``` -pub fn concat(left: List, right: List) -> List { - when left is { - [] -> right +pub fn has(self: List, elem: a) -> Bool { + when self is { + [] -> False [x, ..xs] -> - [x, ..concat(xs, right)] + if x == elem { + True + } else { + has(xs, elem) + } } } -test concat_1() { - concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +test has_1() { + has([1, 2, 3], 1) == True } -test concat_2() { - concat([1, 2, 3], []) == [1, 2, 3] +test has_2() { + has([1, 2, 3], 14) == False } -test concat_3() { - concat([], [1, 2, 3]) == [1, 2, 3] +test has_3() { + has([], 14) == False } -/// Remove the first occurrence of the given element from the list. +/// Get the first element of a list /// /// ```aiken -/// list.delete([1, 2, 3, 1], 1) == [2, 3, 1] -/// list.delete([1, 2, 3], 14) == [1, 2, 3] +/// list.head([1, 2, 3]) == Some(1) +/// list.head([]) == None /// ``` -pub fn delete(self: List, elem: a) -> List { +pub fn head(self: List) -> Option { when self is { - [] -> - [] - [x, ..xs] -> - if x == elem { - xs - } else { - [x, ..delete(xs, elem)] - } + [] -> None + _ -> Some(builtin.head_list(self)) } } -test delete_1() { - delete([], 42) == [] +test head_1() { + head([1, 2, 3]) == Some(1) } -test delete_2() { - delete([1, 2, 3, 1], 1) == [2, 3, 1] +test head_2() { + head([]) == None } -test delete_3() { - delete([1, 2, 3], 14) == [1, 2, 3] +/// Checks whether a list is empty. +/// +/// ```aiken +/// list.is_empty([]) == True +/// list.is_empty([1, 2, 3]) == False +/// ``` +pub fn is_empty(self: List) -> Bool { + when self is { + [] -> True + _ -> False + } } -test delete_4() { - delete([2], 2) == [] +test is_empty_1() { + is_empty([]) == True } -/// Remove the first occurrence of each element of the second list from the first one. +test is_empty_2() { + is_empty([1, 2, 3]) == False +} + +/// Gets the index of an element of a list, if any. Otherwise, returns None. /// +/// ```aiken +/// list.index_of([1, 5, 2], 2) == Some(2) +/// list.index_of([1, 7, 3], 4) == None +/// list.index_of([1, 0, 9, 6], 6) == 3 +/// list.index_of([], 6) == None /// ``` -/// list.difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] -/// list.difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] -/// list.difference([1, 2, 3], []) == [1, 2, 3] -/// ``` -pub fn difference(self: List, with: List) -> List { - when with is { - [] -> self - [x, ..xs] -> difference(delete(self, x), xs) +pub fn index_of(self: List, elem: a) -> Option { + do_index_of(self, elem, 0) +} + +fn do_index_of(self: List, elem: a, i: Int) -> Option { + when self is { + [] -> None + [x, ..xs] -> + if x == elem { + Some(i) + } else { + do_index_of(xs, elem, i + 1) + } } } -test difference_1() { - difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] +test index_of_1() { + index_of([1, 5, 2], 2) == Some(2) } -test difference_2() { - difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] +test index_of_2() { + index_of([1, 7, 3], 4) == None } -test difference_3() { - difference([1, 2, 3], []) == [1, 2, 3] +test index_of_3() { + index_of([1, 0, 9, 6], 6) == Some(3) } -test difference_4() { - difference([], [1, 2, 3]) == [] +test index_of_4() { + index_of([], 6) == None +} + +/// Get the last in the given list, if any. +/// +/// ```aiken +/// list.last([]) == None +/// list.last([1, 2, 3]) == Some(3) +/// ``` +pub fn last(self: List) -> Option { + when self is { + [] -> None + [x] -> Some(x) + [_, ..xs] -> last(xs) + } +} + +test last_1() { + last([]) == None +} + +test last_2() { + last([1]) == Some(1) +} + +test last_3() { + last([1, 2, 3, 4]) == Some(4) +} + +/// Get the number of elements in the given list. +/// +/// ```aiken +/// list.length([]) == 0 +/// list.length([1, 2, 3]) == 3 +/// ``` +pub fn length(self: List) -> Int { + when self is { + [] -> 0 + [_, ..xs] -> 1 + length(xs) + } +} + +test length_1() { + length([]) == 0 +} + +test length_2() { + length([1, 2, 3]) == 3 +} + +// ## Modifying + +// ### Extracting + +/// Remove the first occurrence of the given element from the list. +/// +/// ```aiken +/// list.delete([1, 2, 3, 1], 1) == [2, 3, 1] +/// list.delete([1, 2, 3], 14) == [1, 2, 3] +/// ``` +pub fn delete(self: List, elem: a) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + if x == elem { + xs + } else { + [x, ..delete(xs, elem)] + } + } +} + +test delete_1() { + delete([], 42) == [] +} + +test delete_2() { + delete([1, 2, 3, 1], 1) == [2, 3, 1] +} + +test delete_3() { + delete([1, 2, 3], 14) == [1, 2, 3] +} + +test delete_4() { + delete([2], 2) == [] } /// Drop the first `n` elements of a list. @@ -384,113 +586,6 @@ test filter_map_2() { ) == [3, 9, 15] } -/// Find the first element satisfying the given predicate, if any. -/// -/// ```aiken -/// list.find([1, 2, 3], fn(x) { x == 2 }) == Some(2) -/// list.find([4, 5, 6], fn(x) { x == 2 }) == None -/// ``` -pub fn find(self: List, predicate: fn(a) -> Bool) -> Option { - when self is { - [] -> None - [x, ..xs] -> - if predicate(x) { - Some(x) - } else { - find(xs, predicate) - } - } -} - -test find_1() { - find([1, 2, 3], fn(x) { x == 1 }) == Some(1) -} - -test find_2() { - find([1, 2, 3], fn(x) { x > 42 }) == None -} - -test find_3() { - find([], fn(_) { True }) == None -} - -/// Map elements of a list into a new list and flatten the result. -/// -/// ```aiken -/// list.flat_map([1, 2, 3], fn(a) { [a, 2*a] }) == [1, 2, 2, 4, 3, 6] -/// ``` -pub fn flat_map(self: List, with: fn(a) -> List) -> List { - foldr(self, [], fn(x, xs) { concat(with(x), xs) }) -} - -test flat_map_1() { - flat_map([], fn(a) { [a] }) == [] -} - -test flat_map_2() { - flat_map([1, 2, 3], fn(a) { [a, a] }) == [1, 1, 2, 2, 3, 3] -} - -/// Reduce a list from left to right. -/// -/// ```aiken -/// list.foldl([1, 2, 3], 0, fn(n, total) { n + total }) == 6 -/// list.foldl([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [3, 2, 1] -/// ``` -pub fn foldl(self: List, zero: b, with: fn(a, b) -> b) -> b { - when self is { - [] -> zero - [x, ..xs] -> foldl(xs, with(x, zero), with) - } -} - -test foldl_1() { - foldl([], 0, fn(_, _) { 1 }) == 0 -} - -test foldl_2() { - foldl([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 -} - -test foldl_3() { - foldl([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [4, 3, 2, 1] -} - -/// Reduce a list from right to left. -/// -/// ```aiken -/// list.foldr([1, 2, 3], 0, fn(n, total) { n + total }) == 6 -/// list.foldr([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3] -/// ``` -pub fn foldr(self: List, zero: b, with: fn(a, b) -> b) -> b { - when self is { - [] -> zero - [x, ..xs] -> with(x, foldr(xs, zero, with)) - } -} - -test foldr_1() { - foldr([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 -} - -test foldr_2() { - foldr( - [1, 2, 3], - "", - fn(n, _str) { - if builtin.mod_integer(n, 2) == 0 { - "foo" - } else { - "bar" - } - }, - ) == "bar" -} - -test foldr_3() { - foldr([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3, 4] -} - /// Return all elements except the last one. /// /// ```aiken @@ -526,469 +621,50 @@ test init_3() { init([1, 2, 3, 4]) == Some([1, 2, 3]) } -/// Figures out whether a list contain the given element. +/// Returns a tuple with all elements that satisfy the predicate at first +/// element, and the rest as second element. /// /// ```aiken -/// list.has([1, 2, 3], 2) == True -/// list.has([1, 2, 3], 14) == False -/// list.has([], 14) == False +/// list.partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) /// ``` -pub fn has(self: List, elem: a) -> Bool { +pub fn partition(self: List, predicate: fn(a) -> Bool) -> (List, List) { when self is { - [] -> False - [x, ..xs] -> - if x == elem { - True + [] -> ([], []) + [x, ..xs] -> { + let (left, right) = partition(xs, predicate) + if predicate(x) { + ([x, ..left], right) } else { - has(xs, elem) + (left, [x, ..right]) } + } } } -test has_1() { - has([1, 2, 3], 1) == True +test partition_1() { + partition([], fn(x) { x > 2 }) == ([], []) } -test has_2() { - has([1, 2, 3], 14) == False +test partition_2() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + partition(xs, fn(x) { x > 5 }) == ([10, 9, 8, 7, 6], [5, 4, 3, 2, 1]) } -test has_3() { - has([], 14) == False +test partition_3() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + partition(xs, fn(x) { x == 42 }) == ([], xs) } -/// Gets the index of an element of a list, if any. Otherwise, returns None. -/// -/// ```aiken -/// list.index_of([1, 5, 2], 2) == Some(2) -/// list.index_of([1, 7, 3], 4) == None -/// list.index_of([1, 0, 9, 6], 6) == 3 -/// list.index_of([], 6) == None -/// ``` -pub fn index_of(self: List, elem: a) -> Option { - do_index_of(self, elem, 0) +test partition_4() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + partition(xs, fn(x) { x < 42 }) == (xs, []) } -fn do_index_of(self: List, elem: a, i: Int) -> Option { - when self is { - [] -> None - [x, ..xs] -> - if x == elem { - Some(i) - } else { - do_index_of(xs, elem, i + 1) - } - } -} - -test index_of_1() { - index_of([1, 5, 2], 2) == Some(2) -} - -test index_of_2() { - index_of([1, 7, 3], 4) == None -} - -test index_of_3() { - index_of([1, 0, 9, 6], 6) == Some(3) -} - -test index_of_4() { - index_of([], 6) == None -} - -/// Get the first element of a list -/// -/// ```aiken -/// list.head([1, 2, 3]) == Some(1) -/// list.head([]) == None -/// ``` -pub fn head(self: List) -> Option { - when self is { - [] -> None - _ -> Some(builtin.head_list(self)) - } -} - -test head_1() { - head([1, 2, 3]) == Some(1) -} - -test head_2() { - head([]) == None -} - -/// Like [`foldr`](#foldr), but also provides the position (0-based) of the elements when iterating. -/// -/// ```aiken -/// let group = fn(i, x, xs) { [(i, x), ..xs] } -/// list.indexed_foldr(["a", "b", "c"], [], group) == [ -/// (0, "a"), -/// (1, "b"), -/// (2, "c") -/// ] -/// ``` -pub fn indexed_foldr( - self: List, - zero: result, - with: fn(Int, a, result) -> result, -) -> result { - do_indexed_foldr(0, self, zero, with) -} - -fn do_indexed_foldr( - n: Int, - self: List, - zero: result, - with: fn(Int, a, result) -> result, -) -> result { - when self is { - [] -> zero - [x, ..xs] -> with(n, x, do_indexed_foldr(n + 1, xs, zero, with)) - } -} - -test indexed_foldr_1() { - indexed_foldr([], 0, fn(i, x, xs) { i + x + xs }) == 0 -} - -test indexed_foldr_2() { - let letters = - ["a", "b", "c"] - indexed_foldr(letters, [], fn(i, x, xs) { [(i, x), ..xs] }) == [ - (0, "a"), - (1, "b"), - (2, "c"), - ] -} - -/// List [`map`](#map) but provides the position (0-based) of the elements while iterating. -/// -/// ```aiken -/// list.indexed_map([1, 2, 3], fn(i, x) { i + x }) == [1, 3, 5] -/// ``` -pub fn indexed_map(self: List, with: fn(Int, a) -> result) -> List { - do_indexed_map(0, self, with) -} - -fn do_indexed_map( - n: Int, - self: List, - with: fn(Int, a) -> result, -) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - [with(n, x), ..do_indexed_map(n + 1, xs, with)] - } -} - -test indexed_map_1() { - indexed_map([], fn(i, _n) { i }) == [] -} - -test indexed_map_2() { - indexed_map( - [4, 8, 13, 2], - fn(i, n) { - if n == 8 { - n - } else { - i - } - }, - ) == [0, 8, 2, 3] -} - -/// Checks whether a list is empty. -/// -/// ```aiken -/// list.is_empty([]) == True -/// list.is_empty([1, 2, 3]) == False -/// ``` -pub fn is_empty(self: List) -> Bool { - when self is { - [] -> True - _ -> False - } -} - -test is_empty_1() { - is_empty([]) == True -} - -test is_empty_2() { - is_empty([1, 2, 3]) == False -} - -/// Get the last in the given list, if any. -/// -/// ```aiken -/// list.last([]) == None -/// list.last([1, 2, 3]) == Some(3) -/// ``` -pub fn last(self: List) -> Option { - when self is { - [] -> None - [x] -> Some(x) - [_, ..xs] -> last(xs) - } -} - -test last_1() { - last([]) == None -} - -test last_2() { - last([1]) == Some(1) -} - -test last_3() { - last([1, 2, 3, 4]) == Some(4) -} - -/// Get the number of elements in the given list. -/// -/// ```aiken -/// list.length([]) == 0 -/// list.length([1, 2, 3]) == 3 -/// ``` -pub fn length(self: List) -> Int { - when self is { - [] -> 0 - [_, ..xs] -> 1 + length(xs) - } -} - -test length_1() { - length([]) == 0 -} - -test length_2() { - length([1, 2, 3]) == 3 -} - -/// Apply a function to each element of a list. -/// -/// ```aiken -/// list.map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] -/// ``` -pub fn map(self: List, with: fn(a) -> result) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - [with(x), ..map(xs, with)] - } -} - -test map_1() { - map([], fn(n) { n + 1 }) == [] -} - -test map_2() { - map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] -} - -/// Apply a function of two arguments, combining elements from two lists. -/// -/// Note: if one list is longer, the extra elements are dropped. -/// -/// ```aiken -/// list.map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] -/// ``` -pub fn map2( - self: List, - bs: List, - with: fn(a, b) -> result, -) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - when bs is { - [] -> - [] - [y, ..ys] -> - [with(x, y), ..map2(xs, ys, with)] - } - } -} - -test map2_1() { - map2([], [1, 2, 3], fn(a, b) { a + b }) == [] -} - -test map2_2() { - map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] -} - -test map2_3() { - map2([42], [1, 2, 3], fn(_a, b) { Some(b) }) == [Some(1)] -} - -/// Apply a function of three arguments, combining elements from three lists. -/// -/// Note: if one list is longer, the extra elements are dropped. -/// -/// ```aiken -/// list.map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] -/// ``` -pub fn map3( - self: List, - bs: List, - cs: List, - with: fn(a, b, c) -> result, -) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - when bs is { - [] -> - [] - [y, ..ys] -> - when cs is { - [] -> - [] - [z, ..zs] -> - [with(x, y, z), ..map3(xs, ys, zs, with)] - } - } - } -} - -test map3_1() { - map3([], [], [1, 2, 3], fn(a, b, c) { a + b + c }) == [] -} - -test map3_2() { - map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] -} - -/// Add an element in front of the list. Sometimes useful when combined with -/// other functions. -/// -/// ```aiken -/// list.push([2, 3], 1) == [1, ..[2, 3]] == [1, 2, 3] -/// ``` -pub fn push(self: List, elem: a) -> List { - [elem, ..self] -} - -test push_1() { - push([], 0) == [0] -} - -test push_2() { - push([2, 3], 1) == [1, 2, 3] -} - -/// Construct a list of a integer from a given range. -/// -/// ```aiken -/// list.range(0, 3) == [0, 1, 2, 3] -/// list.range(-1, 1) == [-1, 0, 1] -/// ``` -pub fn range(from: Int, to: Int) -> List { - if from > to { - [] - } else { - [from, ..range(from + 1, to)] - } -} - -test range_1() { - range(0, 3) == [0, 1, 2, 3] -} - -test range_2() { - range(-1, 1) == [-1, 0, 1] -} - -/// Construct a list filled with n copies of a value. -/// -/// ```aiken -/// list.repeat("na", 3) == ["na", "na", "na"] -/// ``` -pub fn repeat(elem: a, n_times: Int) -> List { - if n_times <= 0 { - [] - } else { - [elem, ..repeat(elem, n_times - 1)] - } -} - -test repeat_1() { - repeat(42, 0) == [] -} - -test repeat_2() { - repeat(14, 3) == [14, 14, 14] -} - -/// Return the list with its elements in the reserve order. -/// -/// ```aiken -/// list.reverse([1, 2, 3]) == [3, 2, 1] -/// ``` -pub fn reverse(self: List) -> List { - foldl(self, [], fn(x, xs) { [x, ..xs] }) -} - -test reverse_1() { - reverse([]) == [] -} - -test reverse_2() { - reverse([1, 2, 3]) == [3, 2, 1] -} - -/// Returns a tuple with all elements that satisfy the predicate at first -/// element, and the rest as second element. -/// -/// ```aiken -/// list.partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) -/// ``` -pub fn partition(self: List, predicate: fn(a) -> Bool) -> (List, List) { - when self is { - [] -> ([], []) - [x, ..xs] -> { - let (left, right) = partition(xs, predicate) - if predicate(x) { - ([x, ..left], right) - } else { - (left, [x, ..right]) - } - } - } -} - -test partition_1() { - partition([], fn(x) { x > 2 }) == ([], []) -} - -test partition_2() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - partition(xs, fn(x) { x > 5 }) == ([10, 9, 8, 7, 6], [5, 4, 3, 2, 1]) -} - -test partition_3() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - partition(xs, fn(x) { x == 42 }) == ([], xs) -} - -test partition_4() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - partition(xs, fn(x) { x < 42 }) == (xs, []) -} - -test partition_5() { - partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) +test partition_5() { + partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) } /// Extract a sublist from the given list using 0-based indexes. Negative @@ -999,7 +675,7 @@ test partition_5() { /// list.slice([1, 2, 3, 4, 5, 6], from: -2, to: -1) == [5, 6] /// list.slice([1, 2, 3, 4, 5, 6], from: 1, to: -1) == [2, 3, 4, 5, 6] /// ``` -pub fn slice(self: List, from: Int, to: Int) { +pub fn slice(self: List, from: Int, to: Int) -> List { let (i, l) = if from >= 0 { (from, None) @@ -1047,57 +723,6 @@ test slice_6() { slice([1, 2, 3, 4, 5, 6], from: -2, to: 1) == [] } -/// Sort a list in ascending order using the given comparison function. -/// -/// ```aiken -/// use aiken/int -/// -/// sort([3, 1, 4, 0, 2], int.compare) == [0, 1, 2, 3, 4] -/// sort([1, 2, 3], int.compare) == [1, 2, 3] -/// ``` -pub fn sort(self: List, compare: fn(a, a) -> Ordering) -> List { - when self is { - [] -> - [] - [x, ..xs] -> insert(sort(xs, compare), x, compare) - } -} - -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)] - } - } -} - -test sort_1() { - let xs = - [6, 7, 5, 4, 1, 3, 9, 8, 0, 2] - sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -} - -test sort_2() { - let xs = - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -} - -test sort_3() { - let xs = - [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -} - -test sort_4() { - sort([], int.compare) == [] -} - /// Cut a list in two, such that the first list contains the given number of / /// elements and the second list contains the rest. /// @@ -1195,58 +820,282 @@ pub fn take_while(self: List, predicate: fn(a) -> Bool) -> List { [] -> [] [x, ..xs] -> - if predicate(x) { - [x, ..take_while(xs, predicate)] - } else { - [] + if predicate(x) { + [x, ..take_while(xs, predicate)] + } else { + [] + } + } +} + +test take_while_1() { + take_while([], fn(x) { x > 2 }) == [] +} + +test take_while_2() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + take_while(xs, fn(x) { x > 5 }) == [10, 9, 8, 7, 6] +} + +test take_while_3() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + take_while(xs, fn(x) { x == 42 }) == [] +} + +test take_while_4() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + take_while(xs, fn(x) { x < 42 }) == xs +} + +/// Removes duplicate elements from a list. +/// +/// ```aiken +/// list.unique([1, 2, 3, 1]) == [1, 2, 3] +/// ``` +pub fn unique(self: List) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + [x, ..unique(filter(xs, fn(y) { y != x }))] + } +} + +test unique_1() { + unique([]) == [] +} + +test unique_2() { + let xs = + [1, 2, 3, 1, 1, 3, 4, 1, 2, 3, 2, 4, 5, 6, 7, 8, 9, 10, 9] + unique(xs) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +} + +// ### Mapping + +/// Map elements of a list into a new list and flatten the result. +/// +/// ```aiken +/// list.flat_map([1, 2, 3], fn(a) { [a, 2*a] }) == [1, 2, 2, 4, 3, 6] +/// ``` +pub fn flat_map(self: List, with: fn(a) -> List) -> List { + foldr(self, [], fn(x, xs) { concat(with(x), xs) }) +} + +test flat_map_1() { + flat_map([], fn(a) { [a] }) == [] +} + +test flat_map_2() { + flat_map([1, 2, 3], fn(a) { [a, a] }) == [1, 1, 2, 2, 3, 3] +} + +/// List [`map`](#map) but provides the position (0-based) of the elements while iterating. +/// +/// ```aiken +/// list.indexed_map([1, 2, 3], fn(i, x) { i + x }) == [1, 3, 5] +/// ``` +pub fn indexed_map(self: List, with: fn(Int, a) -> result) -> List { + do_indexed_map(0, self, with) +} + +fn do_indexed_map( + n: Int, + self: List, + with: fn(Int, a) -> result, +) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + [with(n, x), ..do_indexed_map(n + 1, xs, with)] + } +} + +test indexed_map_1() { + indexed_map([], fn(i, _n) { i }) == [] +} + +test indexed_map_2() { + indexed_map( + [4, 8, 13, 2], + fn(i, n) { + if n == 8 { + n + } else { + i + } + }, + ) == [0, 8, 2, 3] +} + +/// Apply a function to each element of a list. +/// +/// ```aiken +/// list.map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] +/// ``` +pub fn map(self: List, with: fn(a) -> result) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + [with(x), ..map(xs, with)] + } +} + +test map_1() { + map([], fn(n) { n + 1 }) == [] +} + +test map_2() { + map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] +} + +/// Apply a function of two arguments, combining elements from two lists. +/// +/// Note: if one list is longer, the extra elements are dropped. +/// +/// ```aiken +/// list.map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] +/// ``` +pub fn map2( + self: List, + bs: List, + with: fn(a, b) -> result, +) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + when bs is { + [] -> + [] + [y, ..ys] -> + [with(x, y), ..map2(xs, ys, with)] + } + } +} + +test map2_1() { + map2([], [1, 2, 3], fn(a, b) { a + b }) == [] +} + +test map2_2() { + map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] +} + +test map2_3() { + map2([42], [1, 2, 3], fn(_a, b) { Some(b) }) == [Some(1)] +} + +/// Apply a function of three arguments, combining elements from three lists. +/// +/// Note: if one list is longer, the extra elements are dropped. +/// +/// ```aiken +/// list.map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] +/// ``` +pub fn map3( + self: List, + bs: List, + cs: List, + with: fn(a, b, c) -> result, +) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + when bs is { + [] -> + [] + [y, ..ys] -> + when cs is { + [] -> + [] + [z, ..zs] -> + [with(x, y, z), ..map3(xs, ys, zs, with)] + } } } } -test take_while_1() { - take_while([], fn(x) { x > 2 }) == [] +test map3_1() { + map3([], [], [1, 2, 3], fn(a, b, c) { a + b + c }) == [] } -test take_while_2() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - take_while(xs, fn(x) { x > 5 }) == [10, 9, 8, 7, 6] +test map3_2() { + map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] } -test take_while_3() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - take_while(xs, fn(x) { x == 42 }) == [] +/// Return the list with its elements in the reserve order. +/// +/// ```aiken +/// list.reverse([1, 2, 3]) == [3, 2, 1] +/// ``` +pub fn reverse(self: List) -> List { + foldl(self, [], fn(x, xs) { [x, ..xs] }) } -test take_while_4() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - take_while(xs, fn(x) { x < 42 }) == xs +test reverse_1() { + reverse([]) == [] } -/// Removes duplicate elements from a list. +test reverse_2() { + reverse([1, 2, 3]) == [3, 2, 1] +} + +/// Sort a list in ascending order using the given comparison function. /// /// ```aiken -/// list.unique([1, 2, 3, 1]) == [1, 2, 3] +/// use aiken/int +/// +/// sort([3, 1, 4, 0, 2], int.compare) == [0, 1, 2, 3, 4] +/// sort([1, 2, 3], int.compare) == [1, 2, 3] /// ``` -pub fn unique(self: List) -> List { +pub fn sort(self: List, compare: fn(a, a) -> Ordering) -> List { when self is { [] -> [] + [x, ..xs] -> insert(sort(xs, compare), x, compare) + } +} + +fn insert(self: List, e: a, compare: fn(a, a) -> Ordering) -> List { + when self is { + [] -> + [e] [x, ..xs] -> - [x, ..unique(filter(xs, fn(y) { y != x }))] + if compare(e, x) == Less { + [e, ..self] + } else { + [x, ..insert(xs, e, compare)] + } } } -test unique_1() { - unique([]) == [] +test sort_1() { + let xs = + [6, 7, 5, 4, 1, 3, 9, 8, 0, 2] + sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] } -test unique_2() { +test sort_2() { let xs = - [1, 2, 3, 1, 1, 3, 4, 1, 2, 3, 2, 4, 5, 6, 7, 8, 9, 10, 9] - unique(xs) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +} + +test sort_3() { + let xs = + [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +} + +test sort_4() { + sort([], int.compare) == [] } /// Decompose a list of tuples into a tuple of lists. @@ -1272,6 +1121,65 @@ test unzip_2() { unzip([(1, "a"), (2, "b")]) == ([1, 2], ["a", "b"]) } +// ## Combining + +/// Merge two lists together. +/// +/// ```aiken +/// list.concat([], []) == [] +/// list.concat([], [1, 2, 3]) == [1, 2, 3] +/// list.concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +/// ``` +pub fn concat(left: List, right: List) -> List { + when left is { + [] -> right + [x, ..xs] -> + [x, ..concat(xs, right)] + } +} + +test concat_1() { + concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +} + +test concat_2() { + concat([1, 2, 3], []) == [1, 2, 3] +} + +test concat_3() { + concat([], [1, 2, 3]) == [1, 2, 3] +} + +/// Remove the first occurrence of each element of the second list from the first one. +/// +/// ``` +/// list.difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] +/// list.difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] +/// list.difference([1, 2, 3], []) == [1, 2, 3] +/// ``` +pub fn difference(self: List, with: List) -> List { + when with is { + [] -> self + [x, ..xs] -> difference(delete(self, x), xs) + } +} + +test difference_1() { + difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] +} + +test difference_2() { + difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] +} + +test difference_3() { + difference([1, 2, 3], []) == [1, 2, 3] +} + +test difference_4() { + difference([], [1, 2, 3]) == [] +} + /// Combine two lists together. /// /// Note: if one list is longer, the extra elements are dropped. @@ -1305,6 +1213,112 @@ test zip_3() { zip([1, 2], ["a", "b", "c"]) == [(1, "a"), (2, "b")] } +// ## Transforming + +/// Reduce a list from left to right. +/// +/// ```aiken +/// list.foldl([1, 2, 3], 0, fn(n, total) { n + total }) == 6 +/// list.foldl([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [3, 2, 1] +/// ``` +pub fn foldl(self: List, zero: b, with: fn(a, b) -> b) -> b { + when self is { + [] -> zero + [x, ..xs] -> foldl(xs, with(x, zero), with) + } +} + +test foldl_1() { + foldl([], 0, fn(_, _) { 1 }) == 0 +} + +test foldl_2() { + foldl([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 +} + +test foldl_3() { + foldl([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [4, 3, 2, 1] +} + +/// Reduce a list from right to left. +/// +/// ```aiken +/// list.foldr([1, 2, 3], 0, fn(n, total) { n + total }) == 6 +/// list.foldr([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3] +/// ``` +pub fn foldr(self: List, zero: b, with: fn(a, b) -> b) -> b { + when self is { + [] -> zero + [x, ..xs] -> with(x, foldr(xs, zero, with)) + } +} + +test foldr_1() { + foldr([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 +} + +test foldr_2() { + foldr( + [1, 2, 3], + "", + fn(n, _str) { + if builtin.mod_integer(n, 2) == 0 { + "foo" + } else { + "bar" + } + }, + ) == "bar" +} + +test foldr_3() { + foldr([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3, 4] +} + +/// Like [`foldr`](#foldr), but also provides the position (0-based) of the elements when iterating. +/// +/// ```aiken +/// let group = fn(i, x, xs) { [(i, x), ..xs] } +/// list.indexed_foldr(["a", "b", "c"], [], group) == [ +/// (0, "a"), +/// (1, "b"), +/// (2, "c") +/// ] +/// ``` +pub fn indexed_foldr( + self: List, + zero: result, + with: fn(Int, a, result) -> result, +) -> result { + do_indexed_foldr(0, self, zero, with) +} + +fn do_indexed_foldr( + n: Int, + self: List, + zero: result, + with: fn(Int, a, result) -> result, +) -> result { + when self is { + [] -> zero + [x, ..xs] -> with(n, x, do_indexed_foldr(n + 1, xs, zero, with)) + } +} + +test indexed_foldr_1() { + indexed_foldr([], 0, fn(i, x, xs) { i + x + xs }) == 0 +} + +test indexed_foldr_2() { + let letters = + ["a", "b", "c"] + indexed_foldr(letters, [], fn(i, x, xs) { [(i, x), ..xs] }) == [ + (0, "a"), + (1, "b"), + (2, "c"), + ] +} + /// Reduce a list from left to right using the accumulator as left operand. /// Said differently, this is [`foldl`](#foldl) with callback arguments swapped. /// diff --git a/lib/aiken/collection/pairs.ak b/lib/aiken/collection/pairs.ak index dd67f56..f4ab58b 100644 --- a/lib/aiken/collection/pairs.ak +++ b/lib/aiken/collection/pairs.ak @@ -4,142 +4,177 @@ //// that are specifically tailored to working with associative lists. Fundamentally, a `Pairs` is //// a type-alias to `List>`. //// -//// ### Important -//// -//// Unlike dictionnaries (a.k.a. `Dict`), associative lists make no assumption -//// about the ordering of elements within the list. As a result, lookup -//// functions do traverse the entire list when invoked. They are also not _sets_, -//// and thus allow for duplicate keys. This is reflected in the functions used -//// to interact with them. +//// > [!CAUTION] +//// > +//// > Unlike dictionnaries (a.k.a. [`Dict`](./dict.html#Dict), associative lists make no assumption +//// > about the ordering of elements within the list. As a result, lookup +//// > functions do traverse the entire list when invoked. They are also not _sets_, +//// > and thus allow for duplicate keys. This is reflected in the functions used +//// > to interact with them. -/// Remove a single key-value pair from the `Pairs`. If the key is not found, no changes are made. -/// Duplicate keys are not removed. Only the **first** key found is removed. +// ## Inspecting + +/// Get all values in the alist associated with a given key. /// /// ```aiken -/// pairs.remove_first([], "a") == [] -/// pairs.remove_first([Pair("a", 1)], "a") == [] -/// pairs.remove_first([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] -/// pairs.remove_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2), Pair("a", 3)] +/// pairs.get_all([], "a") == [] +/// pairs.get_all([Pair("a", 1)], "a") == [1] +/// pairs.get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] +/// pairs.get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] /// ``` -pub fn remove_first(self: Pairs, key k: key) -> Pairs { +pub fn get_all(self: Pairs, key k: key) -> List { when self is { [] -> [] - [Pair(k2, v2), ..rest] -> + [Pair(k2, v), ..rest] -> if k == k2 { - rest + [v, ..get_all(rest, k)] } else { - [Pair(k2, v2), ..remove_first(rest, k)] + get_all(rest, k) } } } -test remove_first_1() { - remove_first([], "a") == [] +test get_all_1() { + get_all([], "a") == [] +} + +test get_all_2() { + get_all([Pair("a", 1)], "a") == [1] } -test remove_first_2() { - remove_first([Pair("a", 14)], "a") == [] +test get_all_3() { + get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] } -test remove_first_3() { - let fixture = - [Pair("a", 14)] - remove_first(fixture, "b") == fixture +test get_all_4() { + get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] } -test remove_first_4() { - let fixture = - [Pair("a", 1), Pair("b", 2), Pair("a", 3)] - remove_first(fixture, "a") == [Pair("b", 2), Pair("a", 3)] +test get_all_5() { + get_all([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == [] } -/// Remove a single key-value pair from the Pairs. If the key is not found, no changes are made. -/// Duplicate keys are not removed. Only the **last** key found is removed. +/// Get the value in the alist by its key. +/// If multiple values with the same key exist, only the first one is returned. /// /// ```aiken -/// pairs.remove_last([], "a") == [] -/// pairs.remove_last([Pair("a", 1)], "a") == [] -/// pairs.remove_last([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] -/// pairs.remove_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("a", 1), Pair("b", 2)] +/// pairs.get_first([], "a") == None +/// pairs.get_first([Pair("a", 1)], "a") == Some(1) +/// pairs.get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +/// pairs.get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) /// ``` -pub fn remove_last(self: Pairs, key k: key) -> Pairs { +pub fn get_first(self: Pairs, key k: key) -> Option { when self is { - [] -> - [] - [Pair(k2, v2), ..rest] -> + [] -> None + [Pair(k2, v), ..rest] -> if k == k2 { - let tail = remove_last(rest, k) - if tail == rest { - rest - } else { - [Pair(k2, v2), ..tail] + Some(v) + } else { + get_first(rest, k) + } + } +} + +test get_first_1() { + get_first([], "a") == None +} + +test get_first_2() { + get_first([Pair("a", 1)], "a") == Some(1) +} + +test get_first_3() { + get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +} + +test get_first_4() { + get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) +} + +test get_first_5() { + get_first([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None +} + +/// Get the value in the alist by its key. +/// If multiple values with the same key exist, only the last one is returned. +/// +/// ```aiken +/// pairs.get_last([], "a") == None +/// pairs.get_last([Pair("a", 1)], "a") == Some(1) +/// pairs.get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +/// pairs.get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) +/// ``` +pub fn get_last(self: Pairs, key k: key) -> Option { + when self is { + [] -> None + [Pair(k2, v), ..rest] -> + if k == k2 { + when get_last(rest, k) is { + None -> Some(v) + some -> some } } else { - [Pair(k2, v2), ..remove_last(rest, k)] + get_last(rest, k) } } } -test remove_last_1() { - remove_last([], "a") == [] +test get_last_1() { + get_last([], "a") == None } -test remove_last_2() { - remove_last([Pair("a", 14)], "a") == [] +test get_last_2() { + get_last([Pair("a", 1)], "a") == Some(1) } -test remove_last_3() { - let fixture = - [Pair("a", 14)] - remove_last(fixture, "b") == fixture +test get_last_3() { + get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) } -test remove_last_4() { - let fixture = - [Pair("a", 1), Pair("b", 2), Pair("a", 3)] - remove_last(fixture, "a") == [Pair("a", 1), Pair("b", 2)] +test get_last_4() { + get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) } -/// Remove all key-value pairs matching the key from the Pairs. If the key is not found, no changes are made. +test get_last_5() { + get_last([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None +} + +/// Finds all keys in the alist associated with a given value. /// /// ```aiken -/// pairs.remove_all([], "a") == [] -/// pairs.remove_all([Pair("a", 1)], "a") == [] -/// pairs.remove_all([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] -/// pairs.remove_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2)] +/// pairs.find_all([], 1) == [] +/// pairs.find_all([Pair("a", 1)], 1) == ["a"] +/// pairs.find_all([Pair("a", 1), Pair("b", 2)], 1) == ["a"] +/// pairs.find_all([Pair("a", 1), Pair("b", 2), Pair("c", 1)], 1) == ["a", "c"] /// ``` -pub fn remove_all(self: Pairs, key k: key) -> Pairs { +pub fn find_all(self: Pairs, v: value) -> List { when self is { [] -> [] [Pair(k2, v2), ..rest] -> - if k == k2 { - remove_all(rest, k) + if v == v2 { + [k2, ..find_all(rest, v)] } else { - [Pair(k2, v2), ..remove_all(rest, k)] + find_all(rest, v) } } } -test remove_all_1() { - remove_all([], "a") == [] +test find_all_1() { + find_all([], "a") == [] } -test remove_all_2() { - remove_all([Pair("a", 14)], "a") == [] +test find_all_2() { + find_all([Pair("a", 14)], 14) == ["a"] } -test remove_all_3() { - let fixture = - [Pair("a", 14)] - remove_all(fixture, "b") == fixture +test find_all_3() { + find_all([Pair("a", 14)], 42) == [] } -test remove_all_4() { - let fixture = - [Pair("a", 1), Pair("b", 2), Pair("a", 3)] - remove_all(fixture, "a") == [Pair("b", 2)] +test find_all_4() { + find_all([Pair("a", 14), Pair("b", 42), Pair("c", 14)], 14) == ["a", "c"] } /// Finds the first key in the alist associated with a given value, if any. @@ -217,309 +252,234 @@ test find_last_4() { find_last([Pair("a", 14), Pair("b", 42), Pair("c", 14)], 14) == Some("c") } -/// Finds all keys in the alist associated with a given value. +/// Check if a key exists in the pairs. /// /// ```aiken -/// pairs.find_all([], 1) == [] -/// pairs.find_all([Pair("a", 1)], 1) == ["a"] -/// pairs.find_all([Pair("a", 1), Pair("b", 2)], 1) == ["a"] -/// pairs.find_all([Pair("a", 1), Pair("b", 2), Pair("c", 1)], 1) == ["a", "c"] +/// pairs.has_key([], "a") == False +/// pairs.has_key([Pair("a", 1)], "a") == True +/// pairs.has_key([Pair("a", 1), Pair("b", 2)], "a") == True +/// pairs.has_key([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == True /// ``` -pub fn find_all(self: Pairs, v: value) -> List { +pub fn has_key(self: Pairs, k: key) -> Bool { when self is { - [] -> - [] - [Pair(k2, v2), ..rest] -> - if v == v2 { - [k2, ..find_all(rest, v)] - } else { - find_all(rest, v) - } + [] -> False + // || is lazy so this is fine + [Pair(k2, _), ..rest] -> k == k2 || has_key(rest, k) } } -test find_all_1() { - find_all([], "a") == [] -} - -test find_all_2() { - find_all([Pair("a", 14)], 14) == ["a"] -} - -test find_all_3() { - find_all([Pair("a", 14)], 42) == [] -} - -test find_all_4() { - find_all([Pair("a", 14), Pair("b", 42), Pair("c", 14)], 14) == ["a", "c"] +test has_key_1() { + !has_key([], "a") } -/// Fold over the key-value pairs in a Pairs. The fold direction follows the -/// order of elements in the Pairs and is done from right-to-left. -/// -/// ```aiken -/// let fixture = [ -/// Pair(1, 100), -/// Pair(2, 200), -/// Pair(3, 300), -/// ] -/// -/// pairs.foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 -/// ``` -pub fn foldr( - self: Pairs, - zero: result, - with: fn(key, value, result) -> result, -) -> result { - when self is { - [] -> zero - [Pair(k, v), ..rest] -> with(k, v, foldr(rest, zero, with)) - } +test has_key_2() { + has_key([Pair("a", 14)], "a") } -test foldr_1() { - foldr([], 14, fn(_, _, _) { 42 }) == 14 +test has_key_3() { + !has_key([Pair("a", 14)], "b") } -test foldr_2() { - foldr( - [Pair("a", 42), Pair("b", 14)], - zero: 0, - with: fn(_, v, total) { v + total }, - ) == 56 +test has_key_4() { + has_key([Pair("a", 14), Pair("b", 42)], "b") } -test foldr_3() { - let fixture = - [Pair(1, 100), Pair(2, 200), Pair(3, 300)] - - foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 +test has_key_5() { + has_key([Pair("a", 14), Pair("b", 42), Pair("a", 42)], "a") } -/// Fold over the key-value pairs in a pairs. The fold direction follows keys -/// in ascending order and is done from left-to-right. +/// Extract all the keys present in a given `Pairs`. /// /// ```aiken -/// let fixture = [ -/// Pair(1, 100), -/// Pair(2, 200), -/// Pair(3, 300), -/// ] -/// -/// pairs.foldl(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 +/// pairs.keys([]) == [] +/// pairs.keys([Pair("a", 1)]) == ["a"] +/// pairs.keys([Pair("a", 1), Pair("b", 2)]) == ["a", "b"] +/// pairs.keys([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == ["a", "b", "a"] /// ``` -pub fn foldl( - self: Pairs, - zero: result, - with: fn(key, value, result) -> result, -) -> result { +pub fn keys(self: Pairs) -> List { when self is { - [] -> zero - [Pair(k, v), ..rest] -> foldl(rest, with(k, v, zero), with) + [] -> + [] + [Pair(k, _), ..rest] -> + [k, ..keys(rest)] } } -test foldl_1() { - foldl([], 14, fn(_, _, _) { 42 }) == 14 +test keys_1() { + keys([]) == [] } -test foldl_2() { - foldl( - [Pair("a", 42), Pair("b", 14)], - zero: 0, - with: fn(_, v, total) { v + total }, - ) == 56 +test keys_2() { + keys([Pair("a", 0)]) == ["a"] } -/// Get the value in the alist by its key. -/// If multiple values with the same key exist, only the first one is returned. +test keys_3() { + keys([Pair("a", 0), Pair("b", 0)]) == ["a", "b"] +} + +/// Extract all the values present in a given `Pairs`. /// /// ```aiken -/// pairs.get_first([], "a") == None -/// pairs.get_first([Pair("a", 1)], "a") == Some(1) -/// pairs.get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) -/// pairs.get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) +/// pairs.values([]) == [] +/// pairs.values([Pair("a", 1)]) == [1] +/// pairs.values([Pair("a", 1), Pair("b", 2)]) == [1, 2] +/// pairs.values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] /// ``` -pub fn get_first(self: Pairs, key k: key) -> Option { +pub fn values(self: Pairs) -> List { when self is { - [] -> None - [Pair(k2, v), ..rest] -> - if k == k2 { - Some(v) - } else { - get_first(rest, k) - } + [] -> + [] + [Pair(_, v), ..rest] -> + [v, ..values(rest)] } } -test get_first_1() { - get_first([], "a") == None +test values_1() { + values([]) == [] } -test get_first_2() { - get_first([Pair("a", 1)], "a") == Some(1) +test values_2() { + values([Pair("a", 1)]) == [1] } -test get_first_3() { - get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +test values_3() { + values([Pair("a", 1), Pair("b", 2)]) == [1, 2] } -test get_first_4() { - get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) +test values_4() { + values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] } -test get_first_5() { - get_first([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None -} +// ## Modifying -/// Get the value in the alist by its key. -/// If multiple values with the same key exist, only the last one is returned. +/// Remove all key-value pairs matching the key from the Pairs. If the key is not found, no changes are made. /// /// ```aiken -/// pairs.get_last([], "a") == None -/// pairs.get_last([Pair("a", 1)], "a") == Some(1) -/// pairs.get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) -/// pairs.get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) +/// pairs.delete_all([], "a") == [] +/// pairs.delete_all([Pair("a", 1)], "a") == [] +/// pairs.delete_all([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] +/// pairs.delete_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2)] /// ``` -pub fn get_last(self: Pairs, key k: key) -> Option { +pub fn delete_all(self: Pairs, key k: key) -> Pairs { when self is { - [] -> None - [Pair(k2, v), ..rest] -> + [] -> + [] + [Pair(k2, v2), ..rest] -> if k == k2 { - when get_last(rest, k) is { - None -> Some(v) - some -> some - } + delete_all(rest, k) } else { - get_last(rest, k) + [Pair(k2, v2), ..delete_all(rest, k)] } } } -test get_last_1() { - get_last([], "a") == None +test delete_all_1() { + delete_all([], "a") == [] } -test get_last_2() { - get_last([Pair("a", 1)], "a") == Some(1) -} - -test get_last_3() { - get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +test delete_all_2() { + delete_all([Pair("a", 14)], "a") == [] } -test get_last_4() { - get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) +test delete_all_3() { + let fixture = + [Pair("a", 14)] + delete_all(fixture, "b") == fixture } -test get_last_5() { - get_last([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None +test delete_all_4() { + let fixture = + [Pair("a", 1), Pair("b", 2), Pair("a", 3)] + delete_all(fixture, "a") == [Pair("b", 2)] } -/// Get all values in the alist associated with a given key. +/// Remove a single key-value pair from the `Pairs`. If the key is not found, no changes are made. +/// Duplicate keys are not deleted. Only the **first** key found is deleted. /// /// ```aiken -/// pairs.get_all([], "a") == [] -/// pairs.get_all([Pair("a", 1)], "a") == [1] -/// pairs.get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] -/// pairs.get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] +/// pairs.delete_first([], "a") == [] +/// pairs.delete_first([Pair("a", 1)], "a") == [] +/// pairs.delete_first([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] +/// pairs.delete_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2), Pair("a", 3)] /// ``` -pub fn get_all(self: Pairs, key k: key) -> List { +pub fn delete_first(self: Pairs, key k: key) -> Pairs { when self is { [] -> [] - [Pair(k2, v), ..rest] -> + [Pair(k2, v2), ..rest] -> if k == k2 { - [v, ..get_all(rest, k)] + rest } else { - get_all(rest, k) + [Pair(k2, v2), ..delete_first(rest, k)] } } } -test get_all_1() { - get_all([], "a") == [] +test delete_first_1() { + delete_first([], "a") == [] } -test get_all_2() { - get_all([Pair("a", 1)], "a") == [1] +test delete_first_2() { + delete_first([Pair("a", 14)], "a") == [] } -test get_all_3() { - get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] -} - -test get_all_4() { - get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] -} - -test get_all_5() { - get_all([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == [] -} - -/// Check if a key exists in the pairs. -/// -/// ```aiken -/// pairs.has_key([], "a") == False -/// pairs.has_key([Pair("a", 1)], "a") == True -/// pairs.has_key([Pair("a", 1), Pair("b", 2)], "a") == True -/// pairs.has_key([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == True -/// ``` -pub fn has_key(self: Pairs, k: key) -> Bool { - when self is { - [] -> False - // || is lazy so this is fine - [Pair(k2, _), ..rest] -> k == k2 || has_key(rest, k) - } -} - -test has_key_1() { - !has_key([], "a") -} - -test has_key_2() { - has_key([Pair("a", 14)], "a") -} - -test has_key_3() { - !has_key([Pair("a", 14)], "b") -} - -test has_key_4() { - has_key([Pair("a", 14), Pair("b", 42)], "b") +test delete_first_3() { + let fixture = + [Pair("a", 14)] + delete_first(fixture, "b") == fixture } -test has_key_5() { - has_key([Pair("a", 14), Pair("b", 42), Pair("a", 42)], "a") +test delete_first_4() { + let fixture = + [Pair("a", 1), Pair("b", 2), Pair("a", 3)] + delete_first(fixture, "a") == [Pair("b", 2), Pair("a", 3)] } -/// Extract all the keys present in a given `Pairs`. +/// Remove a single key-value pair from the Pairs. If the key is not found, no changes are made. +/// Duplicate keys are not deleted. Only the **last** key found is deleted. /// /// ```aiken -/// pairs.keys([]) == [] -/// pairs.keys([Pair("a", 1)]) == ["a"] -/// pairs.keys([Pair("a", 1), Pair("b", 2)]) == ["a", "b"] -/// pairs.keys([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == ["a", "b", "a"] +/// pairs.delete_last([], "a") == [] +/// pairs.delete_last([Pair("a", 1)], "a") == [] +/// pairs.delete_last([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] +/// pairs.delete_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("a", 1), Pair("b", 2)] /// ``` -pub fn keys(self: Pairs) -> List { +pub fn delete_last(self: Pairs, key k: key) -> Pairs { when self is { [] -> [] - [Pair(k, _), ..rest] -> - [k, ..keys(rest)] + [Pair(k2, v2), ..rest] -> + if k == k2 { + let tail = delete_last(rest, k) + if tail == rest { + rest + } else { + [Pair(k2, v2), ..tail] + } + } else { + [Pair(k2, v2), ..delete_last(rest, k)] + } } } -test keys_1() { - keys([]) == [] +test delete_last_1() { + delete_last([], "a") == [] } -test keys_2() { - keys([Pair("a", 0)]) == ["a"] +test delete_last_2() { + delete_last([Pair("a", 14)], "a") == [] } -test keys_3() { - keys([Pair("a", 0), Pair("b", 0)]) == ["a", "b"] +test delete_last_3() { + let fixture = + [Pair("a", 14)] + delete_last(fixture, "b") == fixture +} + +test delete_last_4() { + let fixture = + [Pair("a", 1), Pair("b", 2), Pair("a", 3)] + delete_last(fixture, "a") == [Pair("a", 1), Pair("b", 2)] } /// Apply a function to all key-value pairs in a alist, replacing the values. @@ -555,35 +515,81 @@ test map_2() { map(fixture, with: fn(_, v) { v + 1 }) == [Pair("a", 2), Pair("b", 3)] } -/// Extract all the values present in a given `Pairs`. +// ## Transforming + +/// Fold over the key-value pairs in a pairs. The fold direction follows keys +/// in ascending order and is done from left-to-right. /// /// ```aiken -/// pairs.values([]) == [] -/// pairs.values([Pair("a", 1)]) == [1] -/// pairs.values([Pair("a", 1), Pair("b", 2)]) == [1, 2] -/// pairs.values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] +/// let fixture = [ +/// Pair(1, 100), +/// Pair(2, 200), +/// Pair(3, 300), +/// ] +/// +/// pairs.foldl(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 /// ``` -pub fn values(self: Pairs) -> List { +pub fn foldl( + self: Pairs, + zero: result, + with: fn(key, value, result) -> result, +) -> result { when self is { - [] -> - [] - [Pair(_, v), ..rest] -> - [v, ..values(rest)] + [] -> zero + [Pair(k, v), ..rest] -> foldl(rest, with(k, v, zero), with) } } -test values_1() { - values([]) == [] +test foldl_1() { + foldl([], 14, fn(_, _, _) { 42 }) == 14 } -test values_2() { - values([Pair("a", 1)]) == [1] +test foldl_2() { + foldl( + [Pair("a", 42), Pair("b", 14)], + zero: 0, + with: fn(_, v, total) { v + total }, + ) == 56 } -test values_3() { - values([Pair("a", 1), Pair("b", 2)]) == [1, 2] +/// Fold over the key-value pairs in a Pairs. The fold direction follows the +/// order of elements in the Pairs and is done from right-to-left. +/// +/// ```aiken +/// let fixture = [ +/// Pair(1, 100), +/// Pair(2, 200), +/// Pair(3, 300), +/// ] +/// +/// pairs.foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 +/// ``` +pub fn foldr( + self: Pairs, + zero: result, + with: fn(key, value, result) -> result, +) -> result { + when self is { + [] -> zero + [Pair(k, v), ..rest] -> with(k, v, foldr(rest, zero, with)) + } } -test values_4() { - values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] +test foldr_1() { + foldr([], 14, fn(_, _, _) { 42 }) == 14 +} + +test foldr_2() { + foldr( + [Pair("a", 42), Pair("b", 14)], + zero: 0, + with: fn(_, v, total) { v + total }, + ) == 56 +} + +test foldr_3() { + let fixture = + [Pair(1, 100), Pair(2, 200), Pair(3, 300)] + + foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 } diff --git a/lib/aiken/crypto.ak b/lib/aiken/crypto.ak new file mode 100644 index 0000000..46a7dda --- /dev/null +++ b/lib/aiken/crypto.ak @@ -0,0 +1,147 @@ +use aiken/builtin + +pub type VerificationKey = + ByteArray + +pub type VerificationKeyHash = + Hash + +pub type Script = + ByteArray + +pub type ScriptHash = + Hash + +pub type Signature = + ByteArray + +pub type DataHash = + Hash + +/// A `Hash` is nothing more than a `ByteArray`, but it carries extra +/// information for readability. +/// +/// On-chain, any hash digest value is represented as a plain 'ByteArray'. +/// Though in practice, hashes come from different sources and have +/// different semantics. +/// +/// Hence, while this type-alias doesn't provide any strong type-guarantees, +/// it helps writing functions signatures with more meaningful types than mere +/// 'ByteArray'. +/// +/// Compare for example: +/// +/// ```aiken +/// pub type Credential { +/// VerificationKey(ByteArray) +/// Script(ByteArray) +/// } +/// ``` +/// +/// with +/// +/// ```aiken +/// pub type Credential { +/// VerificationKey(Hash) +/// Script(Hash) +/// } +/// ``` +/// +/// Both are strictly equivalent, but the second reads much better. +pub type Hash = + ByteArray + +// ## Hashing + +/// A blake2b-224 hash algorithm. +/// +/// Typically used for: +/// +/// - [`Credential`](../cardano/address.html#Credential) +/// - [`PolicyId`](../cardano/assets.html#PolicyId) +/// +/// Note: there's no function to calculate blake2b-224 hash digests on-chain. +pub opaque type Blake2b_224 { + Blake2b_224 +} + +/// Compute the blake2b-224 hash digest (28 bytes) of some data. +pub fn blake2b_224(bytes: ByteArray) -> Hash { + builtin.blake2b_224(bytes) +} + +/// A blake2b-256 hash algorithm. +/// +/// Typically used for: +/// +/// - [`TransactionId`](../cardano/transaction.html#TransactionId) +pub opaque type Blake2b_256 { + Blake2b_256 +} + +/// Compute the blake2b-256 hash digest (32 bytes) of some data. +pub fn blake2b_256(bytes: ByteArray) -> Hash { + builtin.blake2b_256(bytes) +} + +/// A Keccak-256 hash algorithm. +pub opaque type Keccak_256 { + Keccak_256 +} + +/// Compute the keccak-256 hash digest (32 bytes) of some data. +pub fn keccak_256(bytes: ByteArray) -> Hash { + builtin.keccak_256(bytes) +} + +/// A SHA2-256 hash algorithm. +pub opaque type Sha2_256 { + Sha2_256 +} + +/// Compute the sha2-256 hash digest (32 bytes) of some data. +pub fn sha2_256(bytes: ByteArray) -> Hash { + builtin.sha2_256(bytes) +} + +/// A SHA3-256 hash algorithm. +pub opaque type Sha3_256 { + Sha3_256 +} + +/// Compute the sha3-256 hash digest (32 bytes) of some data. +pub fn sha3_256(bytes: ByteArray) -> Hash { + builtin.sha3_256(bytes) +} + +// ## Verifying signatures + +/// Verify an ECDCA signature (over secp256k1) using the given verification key. +/// Returns `True` when the signature is valid. +pub fn verify_ecdsa_signature( + key: VerificationKey, + msg: ByteArray, + sig: Signature, +) -> Bool { + builtin.verify_ecdsa_secp256k1_signature(key, msg, sig) +} + +/// Verify an Ed25519 signature using the given verification key. +/// Returns `True` when the signature is valid. +pub fn verify_ed25519_signature( + key: VerificationKey, + msg: ByteArray, + sig: Signature, +) -> Bool { + builtin.verify_ed25519_signature(key, msg, sig) +} + +/// Verify a Schnorr signature (over secp256k1) using the given verification key. +/// Returns `True` when the signature is valid. +pub fn verify_schnorr_signature( + key: VerificationKey, + msg: ByteArray, + sig: Signature, +) -> Bool { + builtin.verify_schnorr_secp256k1_signature(key, msg, sig) +} diff --git a/lib/aiken/hash.ak b/lib/aiken/hash.ak deleted file mode 100644 index 4f86027..0000000 --- a/lib/aiken/hash.ak +++ /dev/null @@ -1,83 +0,0 @@ -//// This module defines `Hash`, a self-documenting type-alias with a -//// phantom-type for readability. -//// -//// On-chain, any hash digest value is represented as a plain 'ByteArray'. -//// Though in practice, hashes come from different sources and have -//// different semantics. -//// -//// Hence, while this type-alias doesn't provide any strong type-guarantees, -//// it helps writing functions signatures with more meaningful types than mere -//// 'ByteArray'. -//// -//// Compare for example: -//// -//// ```aiken -//// pub type Credential { -//// VerificationKeyCredential(ByteArray) -//// ScriptCredential(ByteArray) -//// } -//// ``` -//// -//// with -//// -//// ```aiken -//// pub type Credential { -//// VerificationKeyCredential(Hash) -//// ScriptCredential(Hash) -//// } -//// ``` -//// -//// Both are strictly equivalent, but the second reads much better. - -use aiken/builtin - -/// A `Hash` is nothing more than a `ByteArray`, but it carries extra -/// information for readability. -pub type Hash = - ByteArray - -/// A blake2b-224 hash algorithm. -/// -/// Typically used for: -/// -/// - [`Credential`](../aiken/transaction/credential.html#Credential) -/// - [`PolicyId`](../aiken/transaction/value.html#PolicyId) -/// -/// Note: there's no function to calculate blake2b-224 hash digests on-chain. -pub opaque type Blake2b_224 { - Blake2b_224 -} - -/// A blake2b-256 hash algorithm. -/// -/// Typically used for: -/// -/// - [`TransactionId`](../aiken/transaction.html#TransactionId) -pub opaque type Blake2b_256 { - Blake2b_256 -} - -/// Compute the blake2b-256 hash digest of some data. -pub fn blake2b_256(bytes: ByteArray) -> Hash { - builtin.blake2b_256(bytes) -} - -/// A SHA2-256 hash algorithm. -pub opaque type Sha2_256 { - Sha2_256 -} - -/// Compute the sha2-256 hash digest of some data. -pub fn sha2_256(bytes: ByteArray) -> Hash { - builtin.sha2_256(bytes) -} - -/// A SHA3-256 hash algorithm. -pub opaque type Sha3_256 { - Sha3_256 -} - -/// Compute the sha3-256 hash digest of some data. -pub fn sha3_256(bytes: ByteArray) -> Hash { - builtin.sha3_256(bytes) -} diff --git a/lib/aiken/interval.ak b/lib/aiken/interval.ak index 323d5a9..db518e2 100644 --- a/lib/aiken/interval.ak +++ b/lib/aiken/interval.ak @@ -12,6 +12,8 @@ //// executed, but we can reason about the interval bounds to validate pieces of //// logic. +// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. + /// A type to represent intervals of values. Interval are inhabited by a type /// `a` which is useful for non-infinite intervals that have a finite /// lower-bound and/or upper-bound. @@ -48,103 +50,182 @@ pub type IntervalBound { is_inclusive: Bool, } -/// Return the highest bound of the two. +/// A type of interval bound. Where finite, a value of type `a` must be +/// provided. `a` will typically be an `Int`, representing a number of seconds or +/// milliseconds. +pub type IntervalBoundType { + NegativeInfinity + Finite(a) + PositiveInfinity +} + +// ## Constructing + +/// Create an interval that includes all values greater than the given bound. i.e [lower_bound, +INF) /// /// ```aiken -/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } -/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } +/// interval.after(10) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, +/// } +/// ``` +pub fn after(lower_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, + } +} + +/// Create an interval that includes all values after (and not including) the given bound. i.e (lower_bound, +INF) /// -/// interval.max(ib1, ib2) == ib2 +/// ```aiken +/// interval.entirely_after(10) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, +/// } /// ``` -pub fn max( - left: IntervalBound, - right: IntervalBound, -) -> IntervalBound { - when compare_bound(left, right) is { - Less -> right - Equal -> left - Greater -> left +pub fn entirely_after(lower_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: False, + }, + upper_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, } } -/// Return the smallest bound of the two. +/// Create an interval that includes all values before (and including) the given bound. i.e (-INF, upper_bound] /// /// ```aiken -/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } -/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } +/// interval.before(100) == Interval { +/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, +/// } +/// ``` +pub fn before(upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: True, + }, + } +} + +/// Create an interval that includes all values before (and not including) the given bound. i.e (-INF, upper_bound) /// -/// interval.min(ib1, ib2) == ib1 +/// ```aiken +/// interval.entirely_before(10) == Interval { +/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, +/// } /// ``` -pub fn min( - left: IntervalBound, - right: IntervalBound, -) -> IntervalBound { - when compare_bound(left, right) is { - Less -> left - Equal -> left - Greater -> right +pub fn entirely_before(upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: False, + }, } } -fn compare_bound( - left: IntervalBound, - right: IntervalBound, -) -> Ordering { - when compare_bound_type(left.bound_type, right.bound_type) is { - Less -> Less - Greater -> Greater - Equal -> - if left.is_inclusive == right.is_inclusive { - Equal - } else if left.is_inclusive { - Greater - } else { - Less - } +/// Create an interval that includes all values between two bounds, including the bounds. i.e. [lower_bound, upper_bound] +/// +/// ```aiken +/// interval.between(10, 100) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, +/// } +/// ``` +pub fn between(lower_bound: a, upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: True, + }, } } -/// A type of interval bound. Where finite, a value of type `a` must be -/// provided. `a` will typically be an `Int`, representing a number of seconds or -/// milliseconds. -pub type IntervalBoundType { - NegativeInfinity - Finite(a) - PositiveInfinity +/// Create an interval that includes all values between two bounds, excluding the bounds. i.e. (lower_bound, upper_bound) +/// +/// ```aiken +/// interval.entirely_between(10, 100) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, +/// } +/// ``` +pub fn entirely_between(lower_bound: a, upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: False, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: False, + }, + } } -fn compare_bound_type( - left: IntervalBoundType, - right: IntervalBoundType, -) -> Ordering { - when left is { - NegativeInfinity -> - when right is { - NegativeInfinity -> Equal - _ -> Less - } - PositiveInfinity -> - when right is { - PositiveInfinity -> Equal - _ -> Greater - } - Finite(left) -> - when right is { - NegativeInfinity -> Greater - PositiveInfinity -> Less - Finite(right) -> - if left < right { - Less - } else if left == right { - Equal - } else { - Greater - } - } +/// Create an empty interval that contains no value. +/// +/// ```aiken +/// interval.contains(empty(), 0) == False +/// interval.contains(empty(), 1000) == False +/// ``` +pub fn empty() -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, } } -// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. +/// Create an interval that contains every possible values. i.e. (-INF, +INF) +/// +/// ```aiken +/// interval.contains(everything(), 0) == True +/// interval.contains(everything(), 1000) == True +/// ``` +pub fn everything() -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, + } +} + +// ## Inspecting /// Checks whether an element is contained within the interval. /// @@ -152,11 +233,11 @@ fn compare_bound_type( /// let iv = /// Interval { /// lower_bound: IntervalBound { -/// bound_type: Finite(14), +/// bound_type: Finite(14), /// is_inclusive: True /// }, /// upper_bound: IntervalBound { -/// bound_type: Finite(42), +/// bound_type: Finite(42), /// is_inclusive: False /// }, /// } @@ -254,109 +335,51 @@ test contains_12() { !contains(iv, 14) } -/// Create an interval that contains every possible values. i.e. (-INF, +INF) -/// -/// ```aiken -/// interval.contains(everything(), 0) == True -/// interval.contains(everything(), 1000) == True -/// ``` -pub fn everything() -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, - } -} - -/// Create an empty interval that contains no value. -/// -/// ```aiken -/// interval.contains(empty(), 0) == False -/// interval.contains(empty(), 1000) == False -/// ``` -pub fn empty() -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - } -} - -/// Create an interval that includes all values between two bounds, including the bounds. i.e. [lower_bound, upper_bound] +/// Tells whether an interval is empty; i.e. that is contains no value. /// /// ```aiken -/// interval.between(10, 100) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, -/// } -/// ``` -pub fn between(lower_bound: a, upper_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: True, - }, - } -} - -/// Create an interval that includes all values between two bounds, excluding the bounds. i.e. (lower_bound, upper_bound) +/// let iv1 = interval.empty() /// -/// ```aiken -/// interval.entirely_between(10, 100) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, -/// } -/// ``` -pub fn entirely_between(lower_bound: a, upper_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: False, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: False, - }, - } -} - -/// Create an interval that includes all values greater than the given bound. i.e [lower_bound, +INF) +/// let iv2 = Interval { +/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, +/// } /// -/// ```aiken -/// interval.after(10) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, -/// } -/// ``` -pub fn after(lower_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, +/// let iv3 = Interval { +/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, +/// } +/// +/// interval.is_empty(iv1) == True +/// interval.is_empty(iv2) == True +/// interval.is_empty(iv3) == False +/// +/// // Note: Two empty intervals are not necessarily equal. +/// iv1 != iv2 +/// ``` +pub fn is_empty(self: Interval) -> Bool { + let ordering = + compare_bound_type(self.lower_bound.bound_type, self.upper_bound.bound_type) + + when ordering is { + Greater -> True + Equal -> !(self.lower_bound.is_inclusive && self.upper_bound.is_inclusive) + Less -> { + let is_open_interval = + !self.lower_bound.is_inclusive && !self.upper_bound.is_inclusive + if is_open_interval { + when (self.lower_bound.bound_type, self.upper_bound.bound_type) is { + (Finite(lower_bound), Finite(upper_bound)) -> + lower_bound + 1 == upper_bound + _ -> False + } + } else { + False + } + } } } -// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. - /// Check whether the interval is entirely after the point "a" /// /// ```aiken @@ -413,8 +436,6 @@ test is_entirely_after_9() { !is_entirely_after(entirely_before(10), 5) } -// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. - /// Check whether the interval is entirely before the point "a" /// /// ```aiken @@ -471,112 +492,46 @@ test is_entirely_before_9() { !is_entirely_before(entirely_after(10), 5) } -/// Create an interval that includes all values after (and not including) the given bound. i.e (lower_bound, +INF) -/// -/// ```aiken -/// interval.entirely_after(10) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, -/// } -/// ``` -pub fn entirely_after(lower_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: False, - }, - upper_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, - } -} +// ## Combining -/// Create an interval that includes all values before (and including) the given bound. i.e (-INF, upper_bound] +/// Computes the smallest interval containing the two given intervals, if any /// /// ```aiken -/// interval.before(100) == Interval { -/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, -/// } +/// let iv1 = between(0, 10) +/// let iv2 = between(2, 14) +/// hull(iv1, iv2) == between(0, 14) +/// +/// let iv1 = between(5, 10) +/// let iv2 = before(0) +/// hull(iv1, iv2) == before(10) +/// +/// let iv1 = entirely_after(0) +/// let iv2 = between(10, 42) +/// hull(iv1, iv2) = entirely_after(0) /// ``` -pub fn before(upper_bound: a) -> Interval { +pub fn hull(iv1: Interval, iv2: Interval) -> Interval { Interval { - lower_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: True, - }, + lower_bound: min(iv1.lower_bound, iv2.lower_bound), + upper_bound: max(iv1.upper_bound, iv2.upper_bound), } } -/// Create an interval that includes all values before (and not including) the given bound. i.e (-INF, upper_bound) -/// -/// ```aiken -/// interval.entirely_before(10) == Interval { -/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, -/// } -/// ``` -pub fn entirely_before(upper_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: False, - }, - } +test hull_1() { + let iv1 = between(0, 10) + let iv2 = between(2, 14) + hull(iv1, iv2) == between(0, 14) } -/// Tells whether an interval is empty; i.e. that is contains no value. -/// -/// ```aiken -/// let iv1 = interval.empty() -/// -/// let iv2 = Interval { -/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, -/// } -/// -/// let iv3 = Interval { -/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, -/// } -/// -/// interval.is_empty(iv1) == True -/// interval.is_empty(iv2) == True -/// interval.is_empty(iv3) == False -/// -/// // Note: Two empty intervals are not necessarily equal. -/// iv1 != iv2 -/// ``` -pub fn is_empty(self: Interval) -> Bool { - let ordering = - compare_bound_type(self.lower_bound.bound_type, self.upper_bound.bound_type) +test hull_2() { + let iv1 = between(5, 10) + let iv2 = before(0) + hull(iv1, iv2) == before(10) +} - when ordering is { - Greater -> True - Equal -> !(self.lower_bound.is_inclusive && self.upper_bound.is_inclusive) - Less -> { - let is_open_interval = - !self.lower_bound.is_inclusive && !self.upper_bound.is_inclusive - if is_open_interval { - when (self.lower_bound.bound_type, self.upper_bound.bound_type) is { - (Finite(lower_bound), Finite(upper_bound)) -> - lower_bound + 1 == upper_bound - _ -> False - } - } else { - False - } - } - } +test hull_3() { + let iv1 = entirely_after(0) + let iv2 = between(10, 42) + hull(iv1, iv2) == entirely_after(0) } /// Computes the largest interval contains in the two given intervals, if any. @@ -639,42 +594,89 @@ test intersection_6() { intersection(iv1, iv2) == entirely_between(0, 10) } -/// Computes the smallest interval containing the two given intervals, if any +/// Return the highest bound of the two. /// /// ```aiken -/// let iv1 = between(0, 10) -/// let iv2 = between(2, 14) -/// hull(iv1, iv2) == between(0, 14) -/// -/// let iv1 = between(5, 10) -/// let iv2 = before(0) -/// hull(iv1, iv2) == before(10) +/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } +/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } /// -/// let iv1 = entirely_after(0) -/// let iv2 = between(10, 42) -/// hull(iv1, iv2) = entirely_after(0) +/// interval.max(ib1, ib2) == ib2 /// ``` -pub fn hull(iv1: Interval, iv2: Interval) -> Interval { - Interval { - lower_bound: min(iv1.lower_bound, iv2.lower_bound), - upper_bound: max(iv1.upper_bound, iv2.upper_bound), +pub fn max( + left: IntervalBound, + right: IntervalBound, +) -> IntervalBound { + when compare_bound(left, right) is { + Less -> right + Equal -> left + Greater -> left } } -test hull_1() { - let iv1 = between(0, 10) - let iv2 = between(2, 14) - hull(iv1, iv2) == between(0, 14) +/// Return the smallest bound of the two. +/// +/// ```aiken +/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } +/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } +/// +/// interval.min(ib1, ib2) == ib1 +/// ``` +pub fn min( + left: IntervalBound, + right: IntervalBound, +) -> IntervalBound { + when compare_bound(left, right) is { + Less -> left + Equal -> left + Greater -> right + } } -test hull_2() { - let iv1 = between(5, 10) - let iv2 = before(0) - hull(iv1, iv2) == before(10) +fn compare_bound( + left: IntervalBound, + right: IntervalBound, +) -> Ordering { + when compare_bound_type(left.bound_type, right.bound_type) is { + Less -> Less + Greater -> Greater + Equal -> + if left.is_inclusive == right.is_inclusive { + Equal + } else if left.is_inclusive { + Greater + } else { + Less + } + } } -test hull_3() { - let iv1 = entirely_after(0) - let iv2 = between(10, 42) - hull(iv1, iv2) == entirely_after(0) +fn compare_bound_type( + left: IntervalBoundType, + right: IntervalBoundType, +) -> Ordering { + when left is { + NegativeInfinity -> + when right is { + NegativeInfinity -> Equal + _ -> Less + } + PositiveInfinity -> + when right is { + PositiveInfinity -> Equal + _ -> Greater + } + Finite(left) -> + when right is { + NegativeInfinity -> Greater + PositiveInfinity -> Less + Finite(right) -> + if left < right { + Less + } else if left == right { + Equal + } else { + Greater + } + } + } } diff --git a/lib/aiken/math.ak b/lib/aiken/math.ak index 764152b..dd575e7 100644 --- a/lib/aiken/math.ak +++ b/lib/aiken/math.ak @@ -71,6 +71,152 @@ test clamp_3() { clamp(7, min: 10, max: 100) == 10 } +/// The greatest common divisor of two integers. +/// +/// ```aiken +/// math.gcd(42, 14) == 14 +/// math.gcd(14, 42) == 14 +/// math.gcd(0, 0) == 0 +/// ``` +pub fn gcd(x: Int, y: Int) -> Int { + abs(do_gcd(x, y)) +} + +fn do_gcd(x: Int, y: Int) -> Int { + when y is { + 0 -> x + _ -> do_gcd(y, x % y) + } +} + +test gcd_test1() { + gcd(10, 300) == 10 +} + +test gcd_test2() { + gcd(-10, 300) == 10 +} + +test gcd_test3() { + gcd(42, 14) == 14 +} + +/// Checks if an integer has a given integer square root x. +/// The check has constant time complexity $O(1)$. +/// +/// ```aiken +/// math.is_sqrt(0, 0) +/// math.is_sqrt(25, 5) +/// !math.is_sqrt(25, -5) +/// math.is_sqrt(44203, 210) +/// ``` +pub fn is_sqrt(self: Int, x: Int) -> Bool { + x * x <= self && ( x + 1 ) * ( x + 1 ) > self +} + +test is_sqrt1() { + is_sqrt(44203, 210) +} + +test is_sqrt2() { + is_sqrt(975461057789971041, 987654321) +} + +/// The logarithm in base `b` of an element using integer divisions. +/// +/// ```aiken +/// math.log(10, base: 2) == 3 +/// math.log(42, base: 2) == 5 +/// math.log(42, base: 3) == 3 +/// math.log(5, base: 0) == 0 +/// math.log(4, base: 4) == 1 +/// math.log(4, base: 42) == 0 +/// ``` +pub fn log(self: Int, base: Int) -> Int { + if base <= 0 { + 0 + } else if self == base { + 1 + } else if self < base { + 0 + } else { + 1 + log(self / base, base) + } +} + +test log_10_2() { + log(10, base: 2) == 3 +} + +test log_42_2() { + log(42, base: 2) == 5 +} + +test log_42_3() { + log(42, base: 3) == 3 +} + +test log_5_0() { + log(5, base: 0) == 0 +} + +test log_4_4() { + log(4, base: 4) == 1 +} + +test log_4_43() { + log(4, base: 43) == 0 +} + +/// The integer logarithm in base 2. Faster than [`log`](#log) in this particular case. +/// +/// ```aiken +/// math.log2(1) == 0 +/// math.log2(2) == 1 +/// math.log2(3) == 1 +/// math.log2(4) == 2 +/// math.log2(256) == 8 +/// math.log2(257) == 8 +/// math.log2(511) == 8 +/// math.log2(1025) == 10 +/// ``` +pub fn log2(x: Int) -> Int { + expect x > 0 + let s = builtin.integer_to_bytearray(True, 0, x) + let len = builtin.length_of_bytearray(s) + let b = builtin.index_bytearray(s, 0) + len * 8 - if b < 2 { + 8 + } else if b < 4 { + 7 + } else if b < 8 { + 6 + } else if b < 16 { + 5 + } else if b < 32 { + 4 + } else if b < 64 { + 3 + } else if b < 128 { + 2 + } else { + 1 + } +} + +test log2_matrix() { + and { + log2(1) == 0, + log2(2) == 1, + log2(3) == 1, + log2(4) == 2, + log2(256) == 8, + log2(257) == 8, + log2(511) == 8, + log2(1025) == 10, + } +} + /// Return the maximum of two integers. pub fn max(a: Int, b: Int) -> Int { if a > b { @@ -166,7 +312,7 @@ test pow_2_42() { } /// Calculates the power of 2 for a given exponent `e`. Much cheaper than -/// using `pow(2, _)` for small exponents (0 < e < 256). +/// using `pow(2, _)` for small exponents $0 < e < 256$. /// /// ```aiken /// math.pow2(-2) == 0 @@ -214,82 +360,6 @@ test pow2_256() { pow2(256) == 115792089237316195423570985008687907853269984665640564039457584007913129639936 } -/// The logarithm in base `b` of an element using integer divisions. -/// -/// ```aiken -/// math.log(10, base: 2) == 3 -/// math.log(42, base: 2) == 5 -/// math.log(42, base: 3) == 3 -/// math.log(5, base: 0) == 0 -/// math.log(4, base: 4) == 1 -/// math.log(4, base: 42) == 0 -/// ``` -pub fn log(self: Int, base: Int) -> Int { - if base <= 0 { - 0 - } else if self == base { - 1 - } else if self < base { - 0 - } else { - 1 + log(self / base, base) - } -} - -test log_10_2() { - log(10, base: 2) == 3 -} - -test log_42_2() { - log(42, base: 2) == 5 -} - -test log_42_3() { - log(42, base: 3) == 3 -} - -test log_5_0() { - log(5, base: 0) == 0 -} - -test log_4_4() { - log(4, base: 4) == 1 -} - -test log_4_43() { - log(4, base: 43) == 0 -} - -/// The greatest common divisor of two integers. -/// -/// ```aiken -/// math.gcd(42, 14) == 14 -/// math.gcd(14, 42) == 14 -/// math.gcd(0, 0) == 0 -/// ``` -pub fn gcd(x: Int, y: Int) -> Int { - abs(do_gcd(x, y)) -} - -fn do_gcd(x: Int, y: Int) -> Int { - when y is { - 0 -> x - _ -> do_gcd(y, x % y) - } -} - -test gcd_test1() { - gcd(10, 300) == 10 -} - -test gcd_test2() { - gcd(-10, 300) == 10 -} - -test gcd_test3() { - gcd(42, 14) == 14 -} - /// Calculates the square root of an integer using the [Babylonian /// method](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method). This returns either the exact result or the smallest integer /// nearest to the square root. @@ -302,6 +372,9 @@ test gcd_test3() { /// math.sqrt(44203) == Some(210) /// math.sqrt(-42) == None /// ``` +/// +/// > [!TIP] +/// > This function can be quite expensive to perform on-chain. Prefer using [`is_sqrt`](#is_sqrt) whenever possible. pub fn sqrt(self: Int) -> Option { if self < 0 { None @@ -349,24 +422,3 @@ test sqrt5() { test sqrt6() { sqrt(-42) == None } - -/// Checks if an integer has a given integer square root x. -/// The check has constant time complexity (O(1)). -/// -/// ```aiken -/// math.is_sqrt(0, 0) -/// math.is_sqrt(25, 5) -/// ! math.is_sqrt(25, -5) -/// math.is_sqrt(44203, 210) -/// ``` -pub fn is_sqrt(self: Int, x: Int) -> Bool { - x * x <= self && ( x + 1 ) * ( x + 1 ) > self -} - -test is_sqrt1() { - is_sqrt(44203, 210) -} - -test is_sqrt2() { - is_sqrt(975461057789971041, 987654321) -} diff --git a/lib/aiken/math/rational.ak b/lib/aiken/math/rational.ak index a5db89e..bf163fb 100644 --- a/lib/aiken/math/rational.ak +++ b/lib/aiken/math/rational.ak @@ -1,13 +1,14 @@ -//// This module implements operations between rational numbers. Internally, rational aren't -//// automatically reduced as this is **only done on-demand**. +//// This module implements operations between rational numbers. //// -//// Thus, for example: -//// -//// ```aiken -//// rational.new(2, 3) != rational.new(4, 6) -//// ``` -//// -//// Comparing rational values should, therefore, only happen after reduction (see [reduce](#reduce)) or via the [compare](#compare) method. +//// > [!CAUTION] Internally, rational aren't automatically reduced as this is **only done on-demand**. +//// > +//// > Thus, for example: +//// > +//// > ```aiken +//// > rational.new(2, 3) != rational.new(4, 6) +//// > ``` +//// > +//// > Comparing rational values should, therefore, only happen after reduction (see [reduce](#reduce)) or via the [compare](#compare) method. use aiken/builtin use aiken/collection/list @@ -20,6 +21,27 @@ pub opaque type Rational { denominator: Int, } +// ## Constructing + +/// Create a new `Rational` from an `Int`. +/// +/// ```aiken +/// Some(rational.from_int(14)) == rational.new(14, 1) +/// Some(rational.from_int(-5)) == rational.new(-5, 1) +/// Some(rational.from_int(0)) == rational.new(0, 1) +/// ``` +pub fn from_int(numerator: Int) -> Rational { + Rational { numerator, denominator: 1 } +} + +test from_int_1() { + and { + (from_int(14) == ratio(14, 1))?, + (from_int(-5) == ratio(-5, 1))?, + (from_int(0) == ratio(0, 1))?, + } +} + /// An unsafe constructor for `Rational` values. Assumes that the following invariants are /// enforced: /// @@ -61,6 +83,40 @@ test new_1() { } } +/// A null `Rational`. +pub fn zero() -> Rational { + Rational { numerator: 0, denominator: 1 } +} + +test zero_1() { + zero() == ratio(0, 1) +} + +// ## Inspecting + +/// Get the denominator of a rational value. +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.denominator(x) == 3 +/// ``` +pub fn denominator(self: Rational) -> Int { + self.denominator +} + +test denominator_1() { + expect Some(x) = new(2, 3) + expect Some(y) = new(-2, 3) + expect Some(z) = new(2, -3) + expect Some(w) = new(-2, -3) + and { + (denominator(x) == 3)?, + (denominator(y) == 3)?, + (denominator(z) == 3)?, + (denominator(w) == 3)?, + } +} + /// Get the numerator of a rational value. /// /// ```aiken @@ -85,73 +141,128 @@ test numerator_1() { } } -/// Get the denominator of a rational value. +// ## Modifying + +/// Absolute value of a `Rational`. /// /// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.denominator(x) == 3 +/// expect Some(x) = rational.new(3, 2) +/// expect Some(y) = rational.new(-3, 2) +/// +/// rational.abs(x) == x +/// rational.abs(y) == x /// ``` -pub fn denominator(self: Rational) -> Int { - self.denominator +pub fn abs(self: Rational) -> Rational { + let Rational { numerator: a_n, denominator: a_d } = self + Rational { numerator: math.abs(a_n), denominator: a_d } } -test denominator_1() { - expect Some(x) = new(2, 3) - expect Some(y) = new(-2, 3) - expect Some(z) = new(2, -3) - expect Some(w) = new(-2, -3) +test abs_examples() { and { - (denominator(x) == 3)?, - (denominator(y) == 3)?, - (denominator(z) == 3)?, - (denominator(w) == 3)?, + (abs(ratio(5, 2)) == ratio(5, 2))?, + (abs(ratio(-5, 2)) == ratio(5, 2))?, + (abs(ratio(5, 2)) == abs(ratio(-5, 2)))?, } } -/// A null `Rational`. -pub fn zero() -> Rational { - Rational { numerator: 0, denominator: 1 } +/// Change the sign of a `Rational`. +/// +/// ```aiken +/// expect Some(x) = rational.new(3, 2) +/// expect Some(y) = rational.new(-3, 2) +/// +/// rational.negate(x) == y +/// rational.negate(y) == x +/// ``` +pub fn negate(a: Rational) -> Rational { + let Rational { numerator: a_n, denominator: a_d } = a + Rational { numerator: -a_n, denominator: a_d } } -test zero_1() { - zero() == ratio(0, 1) +test negate_1() { + and { + (negate(ratio(5, 2)) == ratio(-5, 2))?, + (negate(ratio(-5, 2)) == ratio(5, 2))?, + (negate(negate(ratio(5, 2))) == ratio(5, 2))?, + } } -/// Multiplication: the product of two rational values. +/// Reciprocal of a `Rational` number. That is, a new `Rational` where the +/// numerator and denominator have been swapped. +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 5) +/// rational.reciprocal(x) == rational.new(5, 2) +/// +/// let y = rational.zero() +/// rational.reciprocal(y) == None +/// ``` +pub fn reciprocal(self: Rational) -> Option { + let Rational { numerator: a_n, denominator: a_d } = self + if a_n < 0 { + Some(Rational { numerator: -a_d, denominator: -a_n }) + } else if a_n > 0 { + Some(Rational { numerator: a_d, denominator: a_n }) + } else { + None + } +} + +test reciprocal_1() { + and { + (reciprocal(ratio(5, 2)) == new(2, 5))?, + (reciprocal(ratio(-5, 2)) == new(-2, 5))?, + (reciprocal(ratio(0, 2)) == None)?, + (reciprocal(ratio(2, 3)) == new(3, 2))?, + (reciprocal(ratio(-2, 3)) == new(-3, 2))?, + } +} + +/// Reduce a rational to its irreducible form. This operation makes the +/// numerator and denominator coprime. +/// +/// ```aiken +/// expect Some(x) = rational.new(80, 200) +/// Some(rational.reduce(x)) == rational.new(2, 5) +/// ``` +pub fn reduce(self: Rational) -> Rational { + let Rational { numerator: a_n, denominator: a_d } = self + let d = math.gcd(a_n, a_d) + Rational { numerator: a_n / d, denominator: a_d / d } +} + +test reduce_1() { + and { + (reduce(ratio(80, 200)) == ratio(2, 5))?, + (reduce(ratio(-5, 1)) == ratio(-5, 1))?, + (reduce(ratio(0, 3)) == ratio(0, 1))?, + } +} + +// ## Combining + +// ### Arithmetic operations + +/// Addition: sum of two rational values /// /// ```aiken /// expect Some(x) = rational.new(2, 3) /// expect Some(y) = rational.new(3, 4) /// -/// Some(rational.mul(x, y)) == rational.new(6, 12) +/// Some(rational.add(x, y)) == rational.new(17, 12) /// ``` -pub fn mul(left: Rational, right: Rational) -> Rational { +pub fn add(left: Rational, right: Rational) -> Rational { let Rational { numerator: a_n, denominator: a_d } = left let Rational { numerator: b_n, denominator: b_d } = right - Rational { numerator: a_n * b_n, denominator: a_d * b_d } -} - -test mul_1() { - mul(ratio(2, 3), ratio(3, 4)) == ratio(6, 12) + Rational { numerator: a_n * b_d + b_n * a_d, denominator: a_d * b_d } } -test mul_2() { - mul(ratio(-2, 3), ratio(-3, 4)) == ratio(6, 12) +test add_1() { + add(ratio(2, 3), ratio(3, 4)) == ratio(17, 12) } -test mul_3() { - let result = - ratio(2, 5) - |> mul(ratio(1, 8)) - |> mul(ratio(3, 10)) - |> mul(ratio(21, 100)) - |> mul(ratio(3, 5)) - |> mul(ratio(2, 8)) - |> mul(ratio(4, 10)) - |> mul(ratio(22, 100)) - |> reduce - - result == ratio(2079, 50000000) +test add_2() { + add(ratio(-2, 3), ratio(3, 4)) == ratio(1, 12) } /// Division: quotient of two rational values. Returns `None` when the second @@ -175,26 +286,41 @@ test div_2() { div(ratio(2, 3), ratio(-3, 4)) == new(-8, 9) } -/// Addition: sum of two rational values +/// Multiplication: the product of two rational values. /// /// ```aiken /// expect Some(x) = rational.new(2, 3) /// expect Some(y) = rational.new(3, 4) /// -/// Some(rational.add(x, y)) == rational.new(17, 12) +/// Some(rational.mul(x, y)) == rational.new(6, 12) /// ``` -pub fn add(left: Rational, right: Rational) -> Rational { +pub fn mul(left: Rational, right: Rational) -> Rational { let Rational { numerator: a_n, denominator: a_d } = left let Rational { numerator: b_n, denominator: b_d } = right - Rational { numerator: a_n * b_d + b_n * a_d, denominator: a_d * b_d } + Rational { numerator: a_n * b_n, denominator: a_d * b_d } } -test add_1() { - add(ratio(2, 3), ratio(3, 4)) == ratio(17, 12) +test mul_1() { + mul(ratio(2, 3), ratio(3, 4)) == ratio(6, 12) } -test add_2() { - add(ratio(-2, 3), ratio(3, 4)) == ratio(1, 12) +test mul_2() { + mul(ratio(-2, 3), ratio(-3, 4)) == ratio(6, 12) +} + +test mul_3() { + let result = + ratio(2, 5) + |> mul(ratio(1, 8)) + |> mul(ratio(3, 10)) + |> mul(ratio(21, 100)) + |> mul(ratio(3, 5)) + |> mul(ratio(2, 8)) + |> mul(ratio(4, 10)) + |> mul(ratio(22, 100)) + |> reduce + + result == ratio(2079, 50000000) } /// Subtraction: difference of two rational values @@ -214,375 +340,17 @@ pub fn sub(left: Rational, right: Rational) -> Rational { test sub_1() { sub(ratio(2, 3), ratio(3, 4)) == ratio(-1, 12) } - -test sub_2() { - sub(ratio(2, 3), ratio(-3, 4)) == ratio(17, 12) -} - -test sub_3() { - sub(ratio(-2, 3), ratio(3, 4)) == ratio(-17, 12) -} - -/// Create a new `Rational` from an `Int`. -/// -/// ```aiken -/// Some(rational.from_int(14)) == rational.new(14, 1) -/// Some(rational.from_int(-5)) == rational.new(-5, 1) -/// Some(rational.from_int(0)) == rational.new(0, 1) -/// ``` -pub fn from_int(numerator: Int) -> Rational { - Rational { numerator, denominator: 1 } -} - -test from_int_1() { - and { - (from_int(14) == ratio(14, 1))?, - (from_int(-5) == ratio(-5, 1))?, - (from_int(0) == ratio(0, 1))?, - } -} - -/// Returns the nearest `Int` between zero and a given `Rational`. -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.truncate(x) == 0 -/// -/// expect Some(y) = rational.new(44, 14) -/// rational.truncate(y) == 3 -/// -/// expect Some(z) = rational.new(-14, 3) -/// rational.truncate(z) == -4 -/// ``` -pub fn truncate(self: Rational) -> Int { - let Rational { numerator: a_n, denominator: a_d } = self - builtin.quotient_integer(a_n, a_d) -} - -test truncate_1() { - and { - (truncate(ratio(5, 2)) == 2)?, - (truncate(ratio(5, 3)) == 1)?, - (truncate(ratio(5, 4)) == 1)?, - (truncate(ratio(5, 5)) == 1)?, - (truncate(ratio(5, 6)) == 0)?, - (truncate(ratio(8, 3)) == 2)?, - (truncate(ratio(-14, 3)) == -4)?, - } -} - -/// Returns the greatest `Int` no greater than a given `Rational` -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.floor(x) == 0 -/// -/// expect Some(y) = rational.new(44, 14) -/// rational.floor(y) == 3 -/// -/// expect Some(z) = rational.new(-14, 3) -/// rational.floor(z) == -5 -/// ``` -pub fn floor(self: Rational) -> Int { - let Rational { numerator: a_n, denominator: a_d } = self - a_n / a_d -} - -test floor_1() { - and { - (floor(ratio(5, 2)) == 2)?, - (floor(ratio(5, 3)) == 1)?, - (floor(ratio(5, 4)) == 1)?, - (floor(ratio(5, 5)) == 1)?, - (floor(ratio(5, 6)) == 0)?, - (floor(ratio(8, 3)) == 2)?, - (floor(ratio(-14, 3)) == -5)?, - } -} - -/// Returns the smallest `Int` not less than a given `Rational` -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.ceil(x) == 1 -/// -/// expect Some(y) = rational.new(44, 14) -/// rational.ceil(y) == 4 -/// -/// expect Some(z) = rational.new(-14, 3) -/// rational.ceil(z) == -4 -/// ``` -pub fn ceil(self: Rational) -> Int { - let Rational { numerator, denominator } = self - if builtin.remainder_integer(numerator, denominator) > 0 { - builtin.quotient_integer(numerator, denominator) + 1 - } else { - builtin.quotient_integer(numerator, denominator) - } -} - -test ceil_1() { - and { - (ceil(ratio(13, 5)) == 3)?, - (ceil(ratio(15, 5)) == 3)?, - (ceil(ratio(16, 5)) == 4)?, - (ceil(ratio(-3, 5)) == 0)?, - (ceil(ratio(-5, 5)) == -1)?, - (ceil(ratio(-14, 3)) == -4)?, - (ceil(ratio(-14, 6)) == -2)?, - (ceil(ratio(44, 14)) == 4)?, - } -} - -/// Returns the proper fraction of a given `Rational` `r`. That is, a 2-tuple of -/// an `Int` and `Rational` (n, f) such that: -/// -/// - `r = n + f`; -/// - `n` and `f` have the same sign as `r`; -/// - `f` has an absolute value less than 1. -pub fn proper_fraction(self: Rational) -> (Int, Rational) { - let Rational { numerator, denominator } = self - ( - builtin.quotient_integer(numerator, denominator), - Rational { - numerator: builtin.remainder_integer(numerator, denominator), - denominator, - }, - ) -} - -test proper_fraction_1() { - let r = ratio(10, 7) - let (n, f) = proper_fraction(r) - and { - (n == 1)?, - (f == ratio(3, 7))?, - (r == add(from_int(n), f))?, - } -} - -test proper_fraction_2() { - let r = ratio(-10, 7) - let (n, f) = proper_fraction(r) - and { - (n == -1)?, - (f == ratio(-3, 7))?, - (r == add(from_int(n), f))?, - } -} - -test proper_fraction_3() { - let r = ratio(4, 2) - let (n, f) = proper_fraction(r) - and { - (n == 2)?, - (f == ratio(0, 2))?, - (r == add(from_int(n), f))?, - } -} - -/// Round the argument to the nearest whole number. If the argument is -/// equidistant between two values, the greater value is returned (it -/// rounds half towards positive infinity). -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.round(x) == 1 -/// -/// expect Some(y) = rational.new(3, 2) -/// rational.round(y) == 2 -/// -/// expect Some(z) = rational.new(-3, 2) -/// rational.round(z) == -1 -/// ``` -/// -/// ⚠️ This behaves differently than _Haskell_. If you're coming from -/// `PlutusTx`, beware that in Haskell, rounding on equidistant values depends on the -/// whole number being odd or even. If you need this behaviour, use [`round_even`](#round_even). -pub fn round(self: Rational) -> Int { - let (n, f) = proper_fraction(self) - - let is_negative = f.numerator < 0 - - when compare(abs(f), ratio(1, 2)) is { - Less -> n - Equal -> - if is_negative { - n - } else { - n + 1 - } - Greater -> - if is_negative { - n - 1 - } else { - n + 1 - } - } -} - -test round_1() { - and { - (round(ratio(10, 7)) == 1)?, - (round(ratio(11, 7)) == 2)?, - (round(ratio(3, 2)) == 2)?, - (round(ratio(5, 2)) == 3)?, - (round(ratio(-3, 2)) == -1)?, - (round(ratio(-2, 3)) == -1)?, - (round(ratio(-10, 7)) == -1)?, - (round(ratio(4, 2)) == 2)?, - } -} - -/// Round the argument to the nearest whole number. If the argument is -/// equidistant between two values, it returns the value that is even (it -/// rounds half to even, also known as 'banker's rounding'). -/// -/// ```aiken -/// expect Some(w) = rational.new(2, 3) -/// rational.round_even(w) == 1 -/// -/// expect Some(x) = rational.new(3, 2) -/// rational.round_even(x) == 2 -/// -/// expect Some(y) = rational.new(5, 2) -/// rational.round_even(y) == 2 -/// -/// expect Some(y) = rational.new(-3, 2) -/// rational.round_even(y) == -2 -/// ``` -pub fn round_even(self: Rational) -> Int { - let (n, f) = proper_fraction(self) - - let m = - when compare(f, ratio(0, 1)) is { - Less -> -1 - _ -> 1 - } - - let is_even = n % 2 == 0 - - when compare(abs(f), ratio(1, 2)) is { - Less -> n - Equal -> - if is_even { - n - } else { - n + m - } - Greater -> n + m - } -} - -test round_even_1() { - and { - (round_even(ratio(10, 7)) == 1)?, - (round_even(ratio(11, 7)) == 2)?, - (round_even(ratio(3, 2)) == 2)?, - (round_even(ratio(5, 2)) == 2)?, - (round_even(ratio(-3, 2)) == -2)?, - (round_even(ratio(-2, 3)) == -1)?, - (round_even(ratio(-10, 7)) == -1)?, - (round_even(ratio(4, 2)) == 2)?, - } -} - -/// Change the sign of a `Rational`. -/// -/// ```aiken -/// expect Some(x) = rational.new(3, 2) -/// expect Some(y) = rational.new(-3, 2) -/// -/// rational.negate(x) == y -/// rational.negate(y) == x -/// ``` -pub fn negate(a: Rational) -> Rational { - let Rational { numerator: a_n, denominator: a_d } = a - Rational { numerator: -a_n, denominator: a_d } -} - -test negate_1() { - and { - (negate(ratio(5, 2)) == ratio(-5, 2))?, - (negate(ratio(-5, 2)) == ratio(5, 2))?, - (negate(negate(ratio(5, 2))) == ratio(5, 2))?, - } -} - -/// Absolute value of a `Rational`. -/// -/// ```aiken -/// expect Some(x) = rational.new(3, 2) -/// expect Some(y) = rational.new(-3, 2) -/// -/// rational.abs(x) == x -/// rational.abs(y) == x -/// ``` -pub fn abs(self: Rational) -> Rational { - let Rational { numerator: a_n, denominator: a_d } = self - Rational { numerator: math.abs(a_n), denominator: a_d } -} - -test abs_examples() { - and { - (abs(ratio(5, 2)) == ratio(5, 2))?, - (abs(ratio(-5, 2)) == ratio(5, 2))?, - (abs(ratio(5, 2)) == abs(ratio(-5, 2)))?, - } -} - -/// Reciprocal of a `Rational` number. That is, a new `Rational` where the -/// numerator and denominator have been swapped. -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 5) -/// rational.reciprocal(x) == rational.new(5, 2) -/// -/// let y = rational.zero() -/// rational.reciprocal(y) == None -/// ``` -pub fn reciprocal(self: Rational) -> Option { - let Rational { numerator: a_n, denominator: a_d } = self - if a_n < 0 { - Some(Rational { numerator: -a_d, denominator: -a_n }) - } else if a_n > 0 { - Some(Rational { numerator: a_d, denominator: a_n }) - } else { - None - } -} - -test reciprocal_1() { - and { - (reciprocal(ratio(5, 2)) == new(2, 5))?, - (reciprocal(ratio(-5, 2)) == new(-2, 5))?, - (reciprocal(ratio(0, 2)) == None)?, - (reciprocal(ratio(2, 3)) == new(3, 2))?, - (reciprocal(ratio(-2, 3)) == new(-3, 2))?, - } -} - -/// Reduce a rational to its irreducible form. This operation makes the -/// numerator and denominator coprime. -/// -/// ```aiken -/// expect Some(x) = rational.new(80, 200) -/// Some(rational.reduce(x)) == rational.new(2, 5) -/// ``` -pub fn reduce(self: Rational) -> Rational { - let Rational { numerator: a_n, denominator: a_d } = self - let d = math.gcd(a_n, a_d) - Rational { numerator: a_n / d, denominator: a_d / d } + +test sub_2() { + sub(ratio(2, 3), ratio(-3, 4)) == ratio(17, 12) } -test reduce_1() { - and { - (reduce(ratio(80, 200)) == ratio(2, 5))?, - (reduce(ratio(-5, 1)) == ratio(-5, 1))?, - (reduce(ratio(0, 3)) == ratio(0, 1))?, - } +test sub_3() { + sub(ratio(-2, 3), ratio(3, 4)) == ratio(-17, 12) } +// ### Ordering + /// Compare two rationals for an ordering. This is safe to use even for /// non-reduced rationals. /// @@ -709,6 +477,8 @@ test compare_with_lt() { lt(x, y)? && !lt(y, x)? && !lt(x, x)? } +// ### Means + /// Calculate the arithmetic mean between two `Rational` values. /// /// ```aiken @@ -805,3 +575,250 @@ test geometric_mean5() { expect Some(yi) = reciprocal(y) geometric_mean(x, yi) == new(258, 9398) } + +// ## Transforming + +/// Returns the smallest `Int` not less than a given `Rational` +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.ceil(x) == 1 +/// +/// expect Some(y) = rational.new(44, 14) +/// rational.ceil(y) == 4 +/// +/// expect Some(z) = rational.new(-14, 3) +/// rational.ceil(z) == -4 +/// ``` +pub fn ceil(self: Rational) -> Int { + let Rational { numerator, denominator } = self + if builtin.remainder_integer(numerator, denominator) > 0 { + builtin.quotient_integer(numerator, denominator) + 1 + } else { + builtin.quotient_integer(numerator, denominator) + } +} + +test ceil_1() { + and { + (ceil(ratio(13, 5)) == 3)?, + (ceil(ratio(15, 5)) == 3)?, + (ceil(ratio(16, 5)) == 4)?, + (ceil(ratio(-3, 5)) == 0)?, + (ceil(ratio(-5, 5)) == -1)?, + (ceil(ratio(-14, 3)) == -4)?, + (ceil(ratio(-14, 6)) == -2)?, + (ceil(ratio(44, 14)) == 4)?, + } +} + +/// Returns the greatest `Int` no greater than a given `Rational` +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.floor(x) == 0 +/// +/// expect Some(y) = rational.new(44, 14) +/// rational.floor(y) == 3 +/// +/// expect Some(z) = rational.new(-14, 3) +/// rational.floor(z) == -5 +/// ``` +pub fn floor(self: Rational) -> Int { + let Rational { numerator: a_n, denominator: a_d } = self + a_n / a_d +} + +test floor_1() { + and { + (floor(ratio(5, 2)) == 2)?, + (floor(ratio(5, 3)) == 1)?, + (floor(ratio(5, 4)) == 1)?, + (floor(ratio(5, 5)) == 1)?, + (floor(ratio(5, 6)) == 0)?, + (floor(ratio(8, 3)) == 2)?, + (floor(ratio(-14, 3)) == -5)?, + } +} + +/// Returns the proper fraction of a given `Rational` `r`. That is, a 2-tuple of +/// an `Int` and `Rational` (n, f) such that: +/// +/// - `r = n + f`; +/// - `n` and `f` have the same sign as `r`; +/// - `f` has an absolute value less than 1. +pub fn proper_fraction(self: Rational) -> (Int, Rational) { + let Rational { numerator, denominator } = self + ( + builtin.quotient_integer(numerator, denominator), + Rational { + numerator: builtin.remainder_integer(numerator, denominator), + denominator, + }, + ) +} + +test proper_fraction_1() { + let r = ratio(10, 7) + let (n, f) = proper_fraction(r) + and { + (n == 1)?, + (f == ratio(3, 7))?, + (r == add(from_int(n), f))?, + } +} + +test proper_fraction_2() { + let r = ratio(-10, 7) + let (n, f) = proper_fraction(r) + and { + (n == -1)?, + (f == ratio(-3, 7))?, + (r == add(from_int(n), f))?, + } +} + +test proper_fraction_3() { + let r = ratio(4, 2) + let (n, f) = proper_fraction(r) + and { + (n == 2)?, + (f == ratio(0, 2))?, + (r == add(from_int(n), f))?, + } +} + +/// Round the argument to the nearest whole number. If the argument is +/// equidistant between two values, the greater value is returned (it +/// rounds half towards positive infinity). +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.round(x) == 1 +/// +/// expect Some(y) = rational.new(3, 2) +/// rational.round(y) == 2 +/// +/// expect Some(z) = rational.new(-3, 2) +/// rational.round(z) == -1 +/// ``` +/// +/// > [!CAUTION] +/// > This behaves differently than _Haskell_. If you're coming from `PlutusTx`, beware that in Haskell, rounding on equidistant values depends on the whole number being odd or even. +/// > If you need this behaviour, use [`round_even`](#round_even). +pub fn round(self: Rational) -> Int { + let (n, f) = proper_fraction(self) + + let is_negative = f.numerator < 0 + + when compare(abs(f), ratio(1, 2)) is { + Less -> n + Equal -> + if is_negative { + n + } else { + n + 1 + } + Greater -> + if is_negative { + n - 1 + } else { + n + 1 + } + } +} + +test round_1() { + and { + (round(ratio(10, 7)) == 1)?, + (round(ratio(11, 7)) == 2)?, + (round(ratio(3, 2)) == 2)?, + (round(ratio(5, 2)) == 3)?, + (round(ratio(-3, 2)) == -1)?, + (round(ratio(-2, 3)) == -1)?, + (round(ratio(-10, 7)) == -1)?, + (round(ratio(4, 2)) == 2)?, + } +} + +/// Round the argument to the nearest whole number. If the argument is +/// equidistant between two values, it returns the value that is even (it +/// rounds half to even, also known as 'banker's rounding'). +/// +/// ```aiken +/// expect Some(w) = rational.new(2, 3) +/// rational.round_even(w) == 1 +/// +/// expect Some(x) = rational.new(3, 2) +/// rational.round_even(x) == 2 +/// +/// expect Some(y) = rational.new(5, 2) +/// rational.round_even(y) == 2 +/// +/// expect Some(y) = rational.new(-3, 2) +/// rational.round_even(y) == -2 +/// ``` +pub fn round_even(self: Rational) -> Int { + let (n, f) = proper_fraction(self) + + let m = + when compare(f, ratio(0, 1)) is { + Less -> -1 + _ -> 1 + } + + let is_even = n % 2 == 0 + + when compare(abs(f), ratio(1, 2)) is { + Less -> n + Equal -> + if is_even { + n + } else { + n + m + } + Greater -> n + m + } +} + +test round_even_1() { + and { + (round_even(ratio(10, 7)) == 1)?, + (round_even(ratio(11, 7)) == 2)?, + (round_even(ratio(3, 2)) == 2)?, + (round_even(ratio(5, 2)) == 2)?, + (round_even(ratio(-3, 2)) == -2)?, + (round_even(ratio(-2, 3)) == -1)?, + (round_even(ratio(-10, 7)) == -1)?, + (round_even(ratio(4, 2)) == 2)?, + } +} + +/// Returns the nearest `Int` between zero and a given `Rational`. +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.truncate(x) == 0 +/// +/// expect Some(y) = rational.new(44, 14) +/// rational.truncate(y) == 3 +/// +/// expect Some(z) = rational.new(-14, 3) +/// rational.truncate(z) == -4 +/// ``` +pub fn truncate(self: Rational) -> Int { + let Rational { numerator: a_n, denominator: a_d } = self + builtin.quotient_integer(a_n, a_d) +} + +test truncate_1() { + and { + (truncate(ratio(5, 2)) == 2)?, + (truncate(ratio(5, 3)) == 1)?, + (truncate(ratio(5, 4)) == 1)?, + (truncate(ratio(5, 5)) == 1)?, + (truncate(ratio(5, 6)) == 0)?, + (truncate(ratio(8, 3)) == 2)?, + (truncate(ratio(-14, 3)) == -4)?, + } +} diff --git a/lib/aiken/option.ak b/lib/aiken/option.ak index 8ad35fd..cf5ef7d 100644 --- a/lib/aiken/option.ak +++ b/lib/aiken/option.ak @@ -1,3 +1,90 @@ +//// A type to capture optional results; useful for handling errors. +//// +//// Note that the `Option` type and its constructors are readily available in Aiken. They are part of the [Prelude](https://aiken-lang.github.io/prelude/aiken.html#Option) module imported by default in every module. + +// ## Inspecting + +/// Asserts whether an option is `None`. +pub fn is_none(self: Option) -> Bool { + when self is { + Some(_) -> False + _ -> True + } +} + +test is_none_1() { + is_none(Some(0)) == False +} + +test is_none_2() { + is_none(None) == True +} + +/// Asserts whether an option is `Some`, irrespective of the value it contains. +pub fn is_some(self: Option) -> Bool { + when self is { + Some(_) -> True + _ -> False + } +} + +test is_some_1() { + is_some(Some(0)) == True +} + +test is_some_2() { + is_some(None) == False +} + +// ## Combining + +/// Chain together many computations that may fail. +/// +/// ```aiken +/// self +/// |> dict.get(policy_id) +/// |> option.and_then(dict.get(_, asset_name)) +/// |> option.or_else(0) +/// ``` +pub fn and_then( + self: Option, + then: fn(a) -> Option, +) -> Option { + when self is { + None -> None + Some(a) -> then(a) + } +} + +fn try_decrement(n: Int) -> Option { + if n > 0 { + Some(n - 1) + } else { + None + } +} + +test and_then_1() { + let result = + None + |> and_then(try_decrement) + result == None +} + +test and_then_2() { + let result = + Some(14) + |> and_then(try_decrement) + result == Some(13) +} + +test and_then_3() { + let result = + Some(0) + |> and_then(try_decrement) + result == None +} + /// Picks the first element which is not None. If there's no such element, return None. /// /// ```aiken @@ -29,47 +116,51 @@ test choice_3() { Some(1) == choice([None, Some(1)]) } -/// Provide a default value, turning an optional value into a normal value. +/// Converts from `Option>` to `Option`. /// /// ```aiken -/// option.or_else(None, "aiken") == "aiken" -/// option.or_else(Some(42), 14) == 42 +/// option.flatten(Some(Some(42))) == Some(42) +/// option.flatten(Some(None)) == None +/// option.flatten(None) == None /// ``` -pub fn or_else(self: Option, default: a) -> a { - when self is { - None -> default - Some(a) -> a +/// +/// Flattening only removes one level of nesting at a time: +/// +/// ```aiken +/// flatten(Some(Some(Some(42)))) == Some(Some(42)) +/// Some(Some(Some(42))) |> flatten |> flatten == Some(42) +/// ``` +pub fn flatten(opt: Option>) -> Option { + when opt is { + Some(inner) -> inner + None -> None } } -test or_else_1() { - or_else(None, "aiken") == "aiken" +test flatten_1() { + let x: Option> = Some(Some(6)) + Some(6) == flatten(x) } -test or_else_2() { - or_else(Some(42), 14) == 42 +test flatten_2() { + let x: Option> = Some(None) + None == flatten(x) } -/// Like [`or_else`](#or_else) but allows returning an `Option`. -/// This is effectively mapping the error branch. -/// -/// ```aiken -/// option.or_try(None, fn(_) { Some("aiken") }) == Some("aiken") -/// option.or_try(Some(42), fn(_) { Some(14) }) == Some(42) -/// ``` -pub fn or_try(self: Option, compute_default: fn() -> Option) -> Option { - when self is { - None -> compute_default() - _ -> self - } +test flatten_3() { + let x: Option> = None + None == flatten(x) } -test or_try_1() { - or_try(None, fn() { Some("aiken") }) == Some("aiken") -} +test flatten_4() { + let x: Option>> = Some(Some(Some(6))) -test or_try_2() { - or_try(Some(42), fn() { fail }) == Some(42) + let result = + x + |> flatten + |> flatten + + Some(6) == result } /// Apply a function to the inner value of an [`Option`](#option) @@ -174,128 +265,48 @@ test map3_3() { map3(Some(14), Some(42), Some(1337), fn(a, b, c) { c - a + b }) == Some(1365) } -/// Chain together many computations that may fail. +/// Like [`or_else`](#or_else) but allows returning an `Option`. +/// This is effectively mapping the error branch. /// /// ```aiken -/// self -/// |> dict.get(policy_id) -/// |> option.and_then(dict.get(_, asset_name)) -/// |> option.or_else(0) +/// option.or_try(None, fn(_) { Some("aiken") }) == Some("aiken") +/// option.or_try(Some(42), fn(_) { Some(14) }) == Some(42) +/// option.or_try(None, fn (_) { fail }) => 💥 /// ``` -pub fn and_then( - self: Option, - then: fn(a) -> Option, -) -> Option { +pub fn or_try(self: Option, compute_default: fn() -> Option) -> Option { when self is { - None -> None - Some(a) -> then(a) - } -} - -fn try_decrement(n: Int) -> Option { - if n > 0 { - Some(n - 1) - } else { - None + None -> compute_default() + _ -> self } } -test and_then_1() { - let result = - None - |> and_then(try_decrement) - result == None +test or_try_1() { + or_try(None, fn() { Some("aiken") }) == Some("aiken") } -test and_then_2() { - let result = - Some(14) - |> and_then(try_decrement) - result == Some(13) +test or_try_2() { + or_try(Some(42), fn() { fail }) == Some(42) } -test and_then_3() { - let result = - Some(0) - |> and_then(try_decrement) - result == None -} +// ## Transforming -/// Converts from `Option>` to `Option`. -/// -/// ```aiken -/// option.flatten(Some(Some(42))) == Some(42) -/// option.flatten(Some(None)) == None -/// option.flatten(None) == None -/// ``` -/// -/// Flattening only removes one level of nesting at a time: +/// Provide a default value, turning an optional value into a normal value. /// /// ```aiken -/// flatten(Some(Some(Some(42)))) == Some(Some(42)) -/// Some(Some(Some(42))) |> flatten |> flatten == Some(42) +/// option.or_else(None, "aiken") == "aiken" +/// option.or_else(Some(42), 14) == 42 /// ``` -pub fn flatten(opt: Option>) -> Option { - when opt is { - Some(inner) -> inner - None -> None - } -} - -test flatten_1() { - let x: Option> = Some(Some(6)) - Some(6) == flatten(x) -} - -test flatten_2() { - let x: Option> = Some(None) - None == flatten(x) -} - -test flatten_3() { - let x: Option> = None - None == flatten(x) -} - -test flatten_4() { - let x: Option>> = Some(Some(Some(6))) - - let result = - x - |> flatten - |> flatten - - Some(6) == result -} - -/// Asserts whether an option is `Some`, irrespective of the value it contains. -pub fn is_some(self: Option) -> Bool { - when self is { - Some(_) -> True - _ -> False - } -} - -test is_some_1() { - is_some(Some(0)) == True -} - -test is_some_2() { - is_some(None) == False -} - -/// Asserts whether an option is `None`. -pub fn is_none(self: Option) -> Bool { +pub fn or_else(self: Option, default: a) -> a { when self is { - Some(_) -> False - _ -> True + None -> default + Some(a) -> a } } -test is_none_1() { - is_none(Some(0)) == False +test or_else_1() { + or_else(None, "aiken") == "aiken" } -test is_none_2() { - is_none(None) == True +test or_else_2() { + or_else(Some(42), 14) == 42 } diff --git a/lib/aiken/primitive/bytearray.ak b/lib/aiken/primitive/bytearray.ak index 982a2fc..181be84 100644 --- a/lib/aiken/primitive/bytearray.ak +++ b/lib/aiken/primitive/bytearray.ak @@ -2,48 +2,288 @@ use aiken/builtin use aiken/math use aiken/option -/// Compare two bytearrays lexicographically. +pub type Byte = + Int + +// ## Constructing + +/// Encode an integer value as a Big-Endian (most-significant bytes first) `ByteArray`. +/// The size is the expected size in number of bytes. +/// +/// > [!IMPORTANT] +/// > This function halts the program if the value cannot fit in the given size. When the +/// > size is _too large_, the array is left-padded with zeroes. /// /// ```aiken -/// bytearray.compare(#"00", #"FF") == Less -/// bytearray.compare(#"42", #"42") == Equal -/// bytearray.compare(#"FF", #"00") == Greater +/// bytearray.from_int_big_endian(1_000_000, 3) == #"0f4240" +/// bytearray.from_int_big_endian(1_000_000, 5) == #"00000f4240" +/// bytearray.from_int_big_endian(0, 8) == #"0000000000000000" +/// bytearray.from_int_big_endian(1_000_000, 1) => 💥 /// ``` -pub fn compare(left: ByteArray, right: ByteArray) -> Ordering { - if builtin.less_than_bytearray(left, right) { - Less - } else if builtin.equals_bytearray(left, right) { - Equal +pub fn from_int_big_endian(self: Int, size: Int) -> ByteArray { + builtin.integer_to_bytearray(True, size, self) +} + +test from_int_big_endian_1() { + from_int_big_endian(1_000_000, 3) == #"0f4240" +} + +test from_int_big_endian_2() { + from_int_big_endian(1_000_000, 5) == #"00000f4240" +} + +test from_int_big_endian_3() { + from_int_big_endian(0, 8) == #"0000000000000000" +} + +test from_int_big_endian_4() fail { + from_int_big_endian(1_000_000, 1) == #"40" +} + +/// Encode an integer value as a Little-Endian (least-significant bytes first) `ByteArray`. +/// The size is the expected size in number of bytes. +/// +/// > [!IMPORTANT] +/// > This function halts the program if the value cannot fit in the given size. When the +/// > size is _too large_, the array is right-padded with zeroes. +/// +/// ```aiken +/// bytearray.from_int_big_endian(1_000_000, 3) == #"0f4240" +/// bytearray.from_int_big_endian(1_000_000, 5) == #"00000f4240" +/// bytearray.from_int_big_endian(0, 8) == #"0000000000000000" +/// bytearray.from_int_big_endian(1_000_000, 1) => 💥 +/// ``` +pub fn from_int_little_endian(self: Int, size: Int) -> ByteArray { + builtin.integer_to_bytearray(False, size, self) +} + +test from_int_little_endian_1() { + from_int_little_endian(1_000_000, 3) == #"40420f" +} + +test from_int_little_endian_2() { + from_int_little_endian(1_000_000, 5) == #"40420f0000" +} + +test from_int_little_endian_3() { + from_int_little_endian(0, 8) == #"0000000000000000" +} + +test from_int_little_endian_4() fail { + from_int_little_endian(1_000_000, 1) == #"40" +} + +/// Convert a `String` into a `ByteArray`. +/// +/// ```aiken +/// bytearray.from_string(@"ABC") == #"414243" +/// ``` +pub fn from_string(str: String) -> ByteArray { + builtin.encode_utf8(str) +} + +test from_string_1() { + from_string(@"") == "" +} + +test from_string_2() { + from_string(@"ABC") == #"414243" +} + +/// Add a byte element in front of a `ByteArray`. When the given byte is +/// greater than 255, it wraps-around. **PlutusV2 behavior** So 256 is mapped to 0, 257 to 1, and so +/// forth. +/// In PlutusV3 this will error instead of wrapping around. +/// +/// ```aiken +/// bytearray.push(#"", 0) == #"00" +/// bytearray.push(#"0203", 1) == #"010203" +/// bytearray.push(#"0203", 257) == #"010203" +/// ``` +pub fn push(self: ByteArray, byte: Byte) -> ByteArray { + builtin.cons_bytearray(byte, self) +} + +test push_1() { + push(#[], 0) == #[0] +} + +test push_2() { + push(#[2, 3], 1) == #[1, 2, 3] +} + +test push_3() { + let x = 257 + push(#[2, 3], x) == #[1, 2, 3] +} + +// ## Inspecting + +/// Get the `Byte` at the given index, or crash. +/// +/// > [!WARNING] +/// > This functions halts the program if there's no byte at the given index. +pub fn at(self: ByteArray, index: Int) -> Byte { + builtin.index_bytearray(self, index) +} + +/// Search the start and end positions of a sub-array in a `ByteArray`. +/// +/// ```aiken +/// bytearray.index_of("Hello, World!", "World") == Some((7, 11)) +/// bytearray.index_of("Hello, World!", "foo") == None +/// bytearray.index_of("Hello, World!", "!") == Some((12, 12)) +/// bytearray.index_of("Hello, World!", "o") == Some((4, 4)) +/// bytearray.index_of("Hello, World!", "Hello, World!") == Some((0, 12)) +/// ``` +pub fn index_of(self: ByteArray, bytes: ByteArray) -> Option<(Int, Int)> { + let offset = length(bytes) + + do_index_of(self, bytes, 0, offset, length(self)) + |> option.map(fn(ix) { (ix, ix + offset - 1) }) +} + +fn do_index_of( + self: ByteArray, + bytes: ByteArray, + cursor: Int, + offset: Int, + size: Int, +) -> Option { + if cursor + offset > size { + None } else { - Greater + if builtin.slice_bytearray(cursor, offset, self) == bytes { + Some(cursor) + } else { + do_index_of(self, bytes, cursor + 1, offset, size) + } } } -/// Combine two `ByteArray` together. +test index_of_1() { + index_of("Hello, World!", "World") == Some((7, 11)) +} + +test index_of_2() { + index_of("Hello, World!", "foo") == None +} + +test index_of_3() { + index_of("Hello, World!", "!") == Some((12, 12)) +} + +test index_of_4() { + index_of("Hello, World!", "o") == Some((4, 4)) +} + +test index_of_5() { + index_of("Hello, World!", "Hello, World!") == Some((0, 12)) +} + +/// Returns `True` when the given `ByteArray` is empty. /// /// ```aiken -/// bytearray.concat(left: #[1, 2, 3], right: #[4, 5, 6]) == #[1, 2, 3, 4, 5, 6] +/// bytearray.is_empty(#"") == True +/// bytearray.is_empty(#"00ff") == False /// ``` -pub fn concat(left: ByteArray, right: ByteArray) -> ByteArray { - builtin.append_bytearray(left, right) +pub fn is_empty(self: ByteArray) -> Bool { + builtin.length_of_bytearray(self) == 0 } -test concat_1() { - concat(#"", #"") == #"" +test is_empty_1() { + is_empty(#"") == True } -test concat_2() { - concat(#"", #"01") == #"01" +test is_empty_2() { + is_empty(#"01") == False } -test concat_3() { - concat(#"0102", #"") == #"0102" +/// Returns the number of bytes in a `ByteArray`. +/// +/// ```aiken +/// bytearray.length(#[1, 2, 3]) == 3 +/// ``` +pub fn length(self: ByteArray) -> Int { + builtin.length_of_bytearray(self) } -test concat_4() { - concat(#"0102", #"0304") == #"01020304" +test length_1() { + length(#"") == 0 +} + +test length_2() { + length(#"010203") == 3 +} + +/// Checks whether a bit (Most-Significant-Bit first) is set in the given 'ByteArray'. +/// +/// For example, consider the following bytearray: `#"8b765f"`. It can also be written as the +/// following bits sequence: +/// +/// `8` | `b` | `7` | `6` | `5` | `f` +/// --- | --- | --- | --- | --- | --- +/// `1000` | `1011` | `0111` | `0110` | `0101` | `1111` +/// +/// And thus, we have: +/// +/// ```aiken +/// test_bit(#"8b765f", 0) == True +/// test_bit(#"8b765f", 1) == False +/// test_bit(#"8b765f", 2) == False +/// test_bit(#"8b765f", 3) == False +/// test_bit(#"8b765f", 7) == True +/// test_bit(#"8b765f", 8) == False +/// test_bit(#"8b765f", 20) == True +/// test_bit(#"8b765f", 21) == True +/// test_bit(#"8b765f", 22) == True +/// test_bit(#"8b765f", 23) == True +/// ``` +pub fn test_bit(self: ByteArray, ix: Int) -> Bool { + builtin.less_than_equals_bytearray( + #[128], + builtin.cons_bytearray( + builtin.index_bytearray(self, ix / 8) * math.pow2(ix % 8), + "", + ), + ) +} + +test test_bit_0() { + test_bit(#"8b765f", 0) } +test test_bit_1() { + !test_bit(#"8b765f", 1) +} + +test test_bit_2() { + !test_bit(#"8b765f", 2) +} + +test test_bit_3() { + !test_bit(#"8b765f", 3) +} + +test test_bit_7() { + test_bit(#"8b765f", 7) +} + +test test_bit_8() { + !test_bit(#"8b765f", 8) +} + +test test_bit_20_21_22_23() { + and { + test_bit(#"8b765f", 20), + test_bit(#"8b765f", 21), + test_bit(#"8b765f", 22), + test_bit(#"8b765f", 23), + } +} + +// ## Modifying + /// Returns the suffix of a `ByteArray` after `n` elements. /// /// ```aiken @@ -68,10 +308,116 @@ test drop_3() { drop(x, 1) == #"" } -test drop_4() { - let x = #"" - drop(x, 2) == #"" -} +test drop_4() { + let x = #"" + drop(x, 2) == #"" +} + +/// Extract a `ByteArray` as a slice of another `ByteArray`. +/// +/// Indexes are 0-based and inclusive. +/// +/// ```aiken +/// bytearray.slice(#[0, 1, 2, 3, 4, 5, 6], start: 1, end: 3) == #[1, 2, 3] +/// ``` +pub fn slice(self: ByteArray, start: Int, end: Int) -> ByteArray { + builtin.slice_bytearray(start, end - start + 1, self) +} + +test slice_1() { + slice(#"", 1, 2) == #"" +} + +test slice_2() { + slice(#"010203", 1, 2) == #"0203" +} + +test slice_3() { + slice(#"010203", 0, 42) == #"010203" +} + +test slice_4() { + slice(#[0, 1, 2, 3, 4], 0, 3) == #[0, 1, 2, 3] +} + +test slice_5() { + slice(#[0, 1, 2, 3, 4], 1, 2) == #[1, 2] +} + +/// Returns the n-length prefix of a `ByteArray`. +/// +/// ```aiken +/// bytearray.take(#[1, 2, 3], n: 2) == #[1, 2] +/// ``` +pub fn take(self: ByteArray, n: Int) -> ByteArray { + builtin.slice_bytearray(0, n, self) +} + +test take_1() { + let x = #"01020304050607" + take(x, 2) == #"0102" +} + +test take_2() { + let x = #"01020304050607" + take(x, 0) == #"" +} + +test take_3() { + let x = #"01" + take(x, 1) == x +} + +test take_4() { + let x = #"010203" + take(x, 0) == #"" +} + +// ## Combining + +/// Combine two `ByteArray` together. +/// +/// ```aiken +/// bytearray.concat(left: #[1, 2, 3], right: #[4, 5, 6]) == #[1, 2, 3, 4, 5, 6] +/// ``` +pub fn concat(left: ByteArray, right: ByteArray) -> ByteArray { + builtin.append_bytearray(left, right) +} + +test concat_1() { + concat(#"", #"") == #"" +} + +test concat_2() { + concat(#"", #"01") == #"01" +} + +test concat_3() { + concat(#"0102", #"") == #"0102" +} + +test concat_4() { + concat(#"0102", #"0304") == #"01020304" +} + +/// Compare two bytearrays lexicographically. +/// +/// ```aiken +/// bytearray.compare(#"00", #"FF") == Less +/// bytearray.compare(#"42", #"42") == Equal +/// bytearray.compare(#"FF", #"00") == Greater +/// ``` +pub fn compare(left: ByteArray, right: ByteArray) -> Ordering { + if builtin.less_than_bytearray(left, right) { + Less + } else if builtin.equals_bytearray(left, right) { + Equal + } else { + Greater + } +} + +// ## Transforming /// Left-fold over bytes of a [`ByteArray`](https://aiken-lang.github.io/prelude/aiken.html#ByteArray). Note that every byte given to the callback function is comprised between 0 and 255. /// @@ -171,139 +517,6 @@ test foldr_3() { foldr(#[1, 2, 3, 4, 5], #"", flip(push)) == #[1, 2, 3, 4, 5] } -/// Search the start and end positions of a sub-array in a `ByteArray`. -/// -/// ```aiken -/// bytearray.index_of("Hello, World!", "World") == Some((7, 11)) -/// bytearray.index_of("Hello, World!", "foo") == None -/// bytearray.index_of("Hello, World!", "!") == Some((12, 12)) -/// bytearray.index_of("Hello, World!", "o") == Some((4, 4)) -/// bytearray.index_of("Hello, World!", "Hello, World!") == Some((0, 12)) -/// ``` -pub fn index_of(self: ByteArray, bytes: ByteArray) -> Option<(Int, Int)> { - let offset = length(bytes) - - do_index_of(self, bytes, 0, offset, length(self)) - |> option.map(fn(ix) { (ix, ix + offset - 1) }) -} - -fn do_index_of( - self: ByteArray, - bytes: ByteArray, - cursor: Int, - offset: Int, - size: Int, -) -> Option { - if cursor + offset > size { - None - } else { - if builtin.slice_bytearray(cursor, offset, self) == bytes { - Some(cursor) - } else { - do_index_of(self, bytes, cursor + 1, offset, size) - } - } -} - -test index_of_1() { - index_of("Hello, World!", "World") == Some((7, 11)) -} - -test index_of_2() { - index_of("Hello, World!", "foo") == None -} - -test index_of_3() { - index_of("Hello, World!", "!") == Some((12, 12)) -} - -test index_of_4() { - index_of("Hello, World!", "o") == Some((4, 4)) -} - -test index_of_5() { - index_of("Hello, World!", "Hello, World!") == Some((0, 12)) -} - -/// Returns the number of bytes in a `ByteArray`. -/// -/// ```aiken -/// bytearray.length(#[1, 2, 3]) == 3 -/// ``` -pub fn length(self: ByteArray) -> Int { - builtin.length_of_bytearray(self) -} - -test length_1() { - length(#"") == 0 -} - -test length_2() { - length(#"010203") == 3 -} - -/// Returns `True` when the given `ByteArray` is empty. -/// -/// ```aiken -/// bytearray.is_empty(#"") == True -/// bytearray.is_empty(#"00ff") == False -/// ``` -pub fn is_empty(self: ByteArray) -> Bool { - builtin.length_of_bytearray(self) == 0 -} - -test is_empty_1() { - is_empty(#"") == True -} - -test is_empty_2() { - is_empty(#"01") == False -} - -/// Convert a `String` into a `ByteArray`. -/// -/// ```aiken -/// bytearray.from_string(@"ABC") == #"414243" -/// ``` -pub fn from_string(str: String) -> ByteArray { - builtin.encode_utf8(str) -} - -test from_string_1() { - from_string(@"") == "" -} - -test from_string_2() { - from_string(@"ABC") == #"414243" -} - -/// Add a byte element in front of a `ByteArray`. When the given byte is -/// greater than 255, it wraps-around. **PlutusV2 behavior** So 256 is mapped to 0, 257 to 1, and so -/// forth. -/// In PlutusV3 this will error instead of wrapping around. -/// -/// ```aiken -/// bytearray.push(#"", 0) == #"00" -/// bytearray.push(#"0203", 1) == #"010203" -/// bytearray.push(#"0203", 257) == #"010203" -/// ``` -pub fn push(self: ByteArray, byte: Int) -> ByteArray { - builtin.cons_bytearray(byte, self) -} - -test push_1() { - push(#[], 0) == #[0] -} - -test push_2() { - push(#[2, 3], 1) == #[1, 2, 3] -} - -test push_3() { - let x = 257 - push(#[2, 3], x) == #[1, 2, 3] -} - /// Reduce bytes in a ByteArray from left to right using the accumulator as left operand. /// Said differently, this is [`foldl`](#foldl) with callback arguments swapped. /// @@ -326,76 +539,62 @@ test reduce_2() { reduce(#[1, 2, 3], #[], push) == #[3, 2, 1] } -/// Extract a `ByteArray` as a slice of another `ByteArray`. -/// -/// Indexes are 0-based and inclusive. +/// Interpret a Big-Endian (most-significant bytes first) `ByteArray` as an `Int`. /// /// ```aiken -/// bytearray.slice(#[0, 1, 2, 3, 4, 5, 6], start: 1, end: 3) == #[1, 2, 3] +/// bytearray.to_int_big_endian(#"0f4240") == 1_000_000 +/// bytearray.to_int_big_endian(#"00000f4240") == 1_000_000 +/// bytearray.to_int_big_endian(#"0000000000000000") == 0 /// ``` -pub fn slice(self: ByteArray, start: Int, end: Int) -> ByteArray { - builtin.slice_bytearray(start, end - start + 1, self) -} - -test slice_1() { - slice(#"", 1, 2) == #"" -} - -test slice_2() { - slice(#"010203", 1, 2) == #"0203" +pub fn to_int_big_endian(self: ByteArray) -> Int { + builtin.bytearray_to_integer(True, self) } -test slice_3() { - slice(#"010203", 0, 42) == #"010203" +test to_int_big_endian_1() { + to_int_big_endian(#"0f4240") == 1_000_000 } -test slice_4() { - slice(#[0, 1, 2, 3, 4], 0, 3) == #[0, 1, 2, 3] +test to_int_big_endian_2() { + to_int_big_endian(#"00000f4240") == 1_000_000 } -test slice_5() { - slice(#[0, 1, 2, 3, 4], 1, 2) == #[1, 2] +test to_int_big_endian_3() { + to_int_big_endian(#"0000000000000000") == 0 } -/// Returns the n-length prefix of a `ByteArray`. +/// Interpret a Little-Endian (least-significant bytes first) `ByteArray` as an `Int`. /// /// ```aiken -/// bytearray.take(#[1, 2, 3], n: 2) == #[1, 2] +/// bytearray.to_int_big_endian(#"40420f") == 1_000_000 +/// bytearray.to_int_big_endian(#"40420f0000") == 1_000_000 +/// bytearray.to_int_big_endian(#"0000000000000000") == 0 /// ``` -pub fn take(self: ByteArray, n: Int) -> ByteArray { - builtin.slice_bytearray(0, n, self) -} - -test take_1() { - let x = #"01020304050607" - take(x, 2) == #"0102" +pub fn to_int_little_endian(self: ByteArray) -> Int { + builtin.bytearray_to_integer(False, self) } -test take_2() { - let x = #"01020304050607" - take(x, 0) == #"" +test to_int_little_endian_1() { + to_int_little_endian(#"40420f") == 1_000_000 } -test take_3() { - let x = #"01" - take(x, 1) == x +test to_int_little_endian_2() { + to_int_little_endian(#"40420f0000") == 1_000_000 } -test take_4() { - let x = #"010203" - take(x, 0) == #"" +test to_int_little_endian_3() { + to_int_little_endian(#"0000000000000000") == 0 } /// Convert a `ByteArray` into a `String`. /// -///
⚠️
WARNING
| This functions fails if the underlying `ByteArray` isn't UTF-8-encoded.
In particular, you cannot convert arbitrary hash digests using this function.
For converting arbitrary `ByteArray`s, use [bytearray.to_hex](#to_hex). -/// --- | --- -/// +/// > [!WARNING] +/// > This functions fails if the underlying `ByteArray` isn't UTF-8-encoded. In particular, you cannot convert arbitrary hash digests using this function. +/// > +/// > For converting arbitrary `ByteArray`s, use [bytearray.to_hex](#to_hex). /// /// ```aiken /// bytearray.to_string(#"414243") == "ABC" -/// -/// bytearray.to_string(some_hash) -> fail +/// bytearray.to_string(some_hash) => 💥 /// ``` pub fn to_string(self: ByteArray) -> String { builtin.decode_utf8(self) @@ -412,8 +611,6 @@ test to_string_2() { /// Encode a `ByteArray` as a hexidecimal `String`. /// /// ```aiken -/// use aiken/bytearray -/// /// bytearray.to_hex("Hello world!") == @"48656c6c6f20776f726c6421" /// ``` pub fn to_hex(self: ByteArray) -> String { @@ -429,69 +626,3 @@ test to_hex_1() { test to_hex_2() { to_hex("The quick brown fox jumps over the lazy dog") == @"54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67" } - -/// Checks whether a bit (Most-Significant-Bit first) is set in the given 'ByteArray'. -/// -/// For example, consider the following bytearray: `#"8b765f"`. It can also be written as the -/// following bits sequence: -/// -/// `8` | `b` | `7` | `6` | `5` | `f` -/// --- | --- | --- | --- | --- | --- -/// `1000` | `1011` | `0111` | `0110` | `0101` | `1111` -/// -/// And thus, we have: -/// -/// ```aiken -/// test_bit(#"8b765f", 0) == True -/// test_bit(#"8b765f", 1) == False -/// test_bit(#"8b765f", 2) == False -/// test_bit(#"8b765f", 3) == False -/// test_bit(#"8b765f", 7) == True -/// test_bit(#"8b765f", 8) == False -/// test_bit(#"8b765f", 20) == True -/// test_bit(#"8b765f", 21) == True -/// test_bit(#"8b765f", 22) == True -/// test_bit(#"8b765f", 23) == True -/// ``` -pub fn test_bit(self: ByteArray, ix: Int) -> Bool { - builtin.less_than_equals_bytearray( - #[128], - builtin.cons_bytearray( - builtin.index_bytearray(self, ix / 8) * math.pow2(ix % 8), - "", - ), - ) -} - -test test_bit_0() { - test_bit(#"8b765f", 0) -} - -test test_bit_1() { - !test_bit(#"8b765f", 1) -} - -test test_bit_2() { - !test_bit(#"8b765f", 2) -} - -test test_bit_3() { - !test_bit(#"8b765f", 3) -} - -test test_bit_7() { - test_bit(#"8b765f", 7) -} - -test test_bit_8() { - !test_bit(#"8b765f", 8) -} - -test test_bit_20_21_22_23() { - and { - test_bit(#"8b765f", 20), - test_bit(#"8b765f", 21), - test_bit(#"8b765f", 22), - test_bit(#"8b765f", 23), - } -} diff --git a/lib/aiken/primitive/int.ak b/lib/aiken/primitive/int.ak index 885f798..217749e 100644 --- a/lib/aiken/primitive/int.ak +++ b/lib/aiken/primitive/int.ak @@ -1,7 +1,10 @@ +use aiken/builtin.{bytearray_to_integer, decode_utf8} use aiken/math use aiken/option use aiken/primitive/bytearray +// ## Combining + /// Compare two integers. /// /// ```aiken @@ -19,7 +22,55 @@ pub fn compare(left: Int, right: Int) -> Ordering { } } -/// Parse an integer from a utf-8 encoded 'ByteArray', when possible. +// ## Transforming + +/// Interpret a Big-Endian (most-significant bytes first) `ByteArray` as an `Int`. +/// +/// ```aiken +/// int.from_bytearray_big_endian(#"0f4240") == 1_000_000 +/// int.from_bytearray_big_endian(#"00000f4240") == 1_000_000 +/// int.from_bytearray_big_endian(#"0000000000000000") == 0 +/// ``` +pub fn from_bytearray_big_endian(self: ByteArray) -> Int { + bytearray_to_integer(True, self) +} + +test from_bytearray_big_endian_1() { + from_bytearray_big_endian(#"0f4240") == 1_000_000 +} + +test from_bytearray_big_endian_2() { + from_bytearray_big_endian(#"00000f4240") == 1_000_000 +} + +test from_bytearray_big_endian_3() { + from_bytearray_big_endian(#"0000000000000000") == 0 +} + +/// Interpret a Little-Endian (least-significant bytes first) `ByteArray` as an `Int`. +/// +/// ```aiken +/// int.from_bytearray_big_endian(#"40420f") == 1_000_000 +/// int.from_bytearray_big_endian(#"40420f0000") == 1_000_000 +/// int.from_bytearray_big_endian(#"0000000000000000") == 0 +/// ``` +pub fn from_bytearray_little_endian(self: ByteArray) -> Int { + bytearray_to_integer(False, self) +} + +test from_bytearray_little_endian_1() { + from_bytearray_little_endian(#"40420f") == 1_000_000 +} + +test from_bytearray_little_endian_2() { + from_bytearray_little_endian(#"40420f0000") == 1_000_000 +} + +test from_bytearray_little_endian_3() { + from_bytearray_little_endian(#"0000000000000000") == 0 +} + +/// Parse an integer from a utf-8 encoded `ByteArray`, when possible. /// /// ```aiken /// int.from_utf8("14") == Some(14) @@ -78,3 +129,28 @@ test from_utf8_5() { test from_utf8_6() { from_utf8("1-2") == None } + +/// Convert an `Int` to its `String` representation. +/// +/// ```aiken +/// int.to_string(42) == @"42" +/// ``` +pub fn to_string(n: Int) -> String { + diagnostic(n, "") |> decode_utf8 +} + +test to_string_1() { + to_string(0) == @"0" +} + +test to_string_2() { + to_string(5) == @"5" +} + +test to_string_3() { + to_string(42) == @"42" +} + +test to_string_4() { + to_string(200) == @"200" +} diff --git a/lib/aiken/primitive/string.ak b/lib/aiken/primitive/string.ak index b9eee08..35fa556 100644 --- a/lib/aiken/primitive/string.ak +++ b/lib/aiken/primitive/string.ak @@ -1,39 +1,19 @@ use aiken/builtin.{ append_bytearray, append_string, decode_utf8, encode_utf8, length_of_bytearray, } -use aiken/cbor -/// Combine two `String` together. -/// -/// ```aiken -/// string.concat(left: @"Hello", right: @", World!") == @"Hello, World!" -/// ``` -pub fn concat(left: String, right: String) -> String { - append_string(left, right) -} - -test concat_1() { - concat(@"", @"") == @"" -} - -test concat_2() { - concat(@"", @"foo") == concat(@"foo", @"") -} - -test concat_3() { - concat(left: @"Hello", right: @", World!") == @"Hello, World!" -} +// ## Constructing /// Convert a `ByteArray` into a `String` /// -///
⚠️
WARNING
| This functions fails if the underlying `ByteArray` isn't UTF-8-encoded.
In particular, you cannot convert arbitrary hash digests using this function.
For converting arbitrary `ByteArray`s, use [bytearray.to_hex](/stdlib/aiken/bytearray.html#to_hex). -/// --- | --- +/// > [!WARNING] +/// > This functions fails if the underlying `ByteArray` isn't UTF-8-encoded. In particular, you cannot convert arbitrary hash digests using this function. +/// > +/// > For converting arbitrary `ByteArray`s, use [bytearray.to_hex](./bytearray.html#to_hex). /// /// ```aiken /// string.from_bytearray("foo") == @"foo" -/// /// string.from_bytearray(#"666f6f") == @"foo" -/// /// string.from_bytearray(some_hash) -> fail /// ``` pub fn from_bytearray(bytes: ByteArray) -> String { @@ -58,7 +38,7 @@ test from_bytearray_3() { /// string.from_int(42) == @"42" /// ``` pub fn from_int(n: Int) -> String { - cbor.diagnostic(n) + diagnostic(n, "") |> decode_utf8 } test from_int_1() { @@ -77,6 +57,29 @@ test from_int_4() { from_int(200) == @"200" } +// ## Combining + +/// Combine two `String` together. +/// +/// ```aiken +/// string.concat(left: @"Hello", right: @", World!") == @"Hello, World!" +/// ``` +pub fn concat(left: String, right: String) -> String { + append_string(left, right) +} + +test concat_1() { + concat(@"", @"") == @"" +} + +test concat_2() { + concat(@"", @"foo") == concat(@"foo", @"") +} + +test concat_3() { + concat(left: @"Hello", right: @", World!") == @"Hello, World!" +} + /// Join a list of strings, separated by a given _delimiter_. /// /// ```aiken @@ -112,6 +115,8 @@ test join_2() { join([@"a", @"b", @"c"], @",") == @"a,b,c" } +// ## Transforming + /// Convert a `String` into a `ByteArray` /// /// ```aiken diff --git a/lib/cardano/credential.ak b/lib/cardano/address.ak similarity index 83% rename from lib/cardano/credential.ak rename to lib/cardano/address.ak index 4cd8fbf..ceef947 100644 --- a/lib/cardano/credential.ak +++ b/lib/cardano/address.ak @@ -1,5 +1,6 @@ -use aiken/builtin -use aiken/hash.{Blake2b_224, Blake2b_256, Hash} +use aiken/crypto.{ + Blake2b_224, Hash, Script, ScriptHash, VerificationKey, VerificationKeyHash, +} /// A general structure for representing an on-chain `Credential`. /// @@ -10,6 +11,8 @@ pub type Credential { Script(ScriptHash) } +// ## Constructing + /// A Cardano `Address` typically holding one or two credential references. /// /// Note that legacy bootstrap addresses (a.k.a. 'Byron addresses') are @@ -21,16 +24,16 @@ pub type Address { stake_credential: Option, } -/// Smart-constructor for an [Address](#Address) from a [verification key](#VerificationKey) hash. The resulting address has no delegation rights whatsoever. -pub fn from_verification_key(vk: Hash) -> Address { - Address { payment_credential: VerificationKey(vk), stake_credential: None } -} - /// Smart-constructor for an [Address](#Address) from a [script](#Script) hash. The address has no delegation rights whatsoever. pub fn from_script(script: Hash) -> Address { Address { payment_credential: Script(script), stake_credential: None } } +/// Smart-constructor for an [Address](#Address) from a [verification key](#VerificationKey) hash. The resulting address has no delegation rights whatsoever. +pub fn from_verification_key(vk: Hash) -> Address { + Address { payment_credential: VerificationKey(vk), stake_credential: None } +} + /// Set (or reset) the delegation part of an [Address](#Address) using a [verification key](#VerificationKey) hash. This is useful when combined with [`from_verification_key`](#from_verification_key) and/or [`from_script`](#from_script). pub fn with_delegation_key( self: Address, @@ -63,25 +66,6 @@ pub type Referenced
{ Pointer { slot_number: Int, transaction_index: Int, certificate_index: Int } } -pub type VerificationKey = - ByteArray - -pub type Script = - ByteArray - -pub type Signature = - ByteArray - -/// Verify an Ed25519 signature using the given verification key. -/// Returns `True` when the signature is valid. -pub fn verify_signature( - key: VerificationKey, - msg: ByteArray, - sig: Signature, -) -> Bool { - builtin.verify_ed25519_signature(key, msg, sig) -} - /// A `StakeCredential` represents the delegation and rewards withdrawal conditions /// associated with some stake address / account. /// @@ -100,15 +84,3 @@ pub type StakeCredential = /// pub type PaymentCredential = Credential - -pub type StakePoolId = - Hash - -pub type VerificationKeyHash = - Hash - -pub type ScriptHash = - Hash - -pub type DatumHash = - Hash diff --git a/lib/cardano/assets.ak b/lib/cardano/assets.ak index f4e9556..1181e1d 100644 --- a/lib/cardano/assets.ak +++ b/lib/cardano/assets.ak @@ -1,8 +1,7 @@ use aiken/collection/dict.{Dict, from_ascending_pairs_with} use aiken/collection/list -use aiken/hash.{Blake2b_224, Hash} +use aiken/crypto.{Blake2b_224, Hash, Script} use aiken/option -use cardano/credential.{Script} /// Lovelace is now a type wrapper for Int. pub type Lovelace = @@ -16,7 +15,7 @@ pub type PolicyId = /// possible to mint Ada!). /// /// By convention, it is an empty `ByteArray`. -pub const ada_policy_id = #"" +pub const ada_policy_id = "" /// A type-alias for 'AssetName`, which are free-form byte-arrays between /// 0 and 32 bytes. @@ -27,25 +26,18 @@ pub type AssetName = /// possible to mint Ada!). /// /// By convention, it is an empty `ByteArray`. -pub const ada_asset_name = #"" +pub const ada_asset_name = "" /// A multi-asset output `Value`. Contains tokens indexed by [PolicyId](#PolicyId) and [AssetName](#AssetName). /// -/// This type maintain some invariants by construction; in particular, a `Value` will never contain a +/// > [!IMPORTANT] +/// > This type maintain some invariants by construction; in particular, a `Value` will never contain a /// zero quantity of a particular token. pub opaque type Value { inner: Dict>, } -/// Construct an empty `Value` with nothing in it. -pub fn zero() -> Value { - Value { inner: dict.new() } -} - -/// Check is a `Value` is zero. That is, it has no assets and holds no Ada/Lovelace. -pub fn is_zero(self: Value) -> Bool { - self == zero() -} +// ## Constructing /// Construct a `Value` from an asset identifier (i.e. `PolicyId` + `AssetName`) /// and a given quantity. @@ -67,136 +59,169 @@ pub fn from_asset( } } -/// Construct a `Value` from a lovelace quantity. +/// Promote an arbitrary list of assets into a `Value`. This function fails +/// (i.e. halt the program execution) if: /// -/// Friendly reminder: 1 Ada = 1.000.000 Lovelace -pub fn from_lovelace(quantity: Int) -> Value { - from_asset(ada_policy_id, ada_asset_name, quantity) +/// - there's any duplicate amongst `PolicyId`; +/// - there's any duplicate amongst `AssetName`; +/// - the `AssetName` aren't sorted in ascending lexicographic order; or +/// - any asset quantity is null. +/// +/// This function is meant to turn arbitrary user-defined `Data` into safe `Value`, +/// while checking for internal invariants. +pub fn from_asset_list(xs: Pairs>) -> Value { + xs + |> list.foldr( + dict.new(), + fn(inner, acc) { + expect Pair(p, [_, ..] as x) = inner + x + |> from_ascending_pairs_with(fn(v) { v != 0 }) + |> dict.insert_with( + acc, + p, + _, + fn(_, _, _) { + fail @"Duplicate policy in the asset list." + }, + ) + }, + ) + |> Value } -/// Get a `Value` excluding Ada. -pub fn without_lovelace(self: Value) -> Value { - dict.delete(self.inner, ada_policy_id) - |> Value +test from_asset_list_1() { + let v = from_asset_list([]) + v == zero() } -test without_lovelace_1() { - let v = from_lovelace(1000000) - without_lovelace(v) == zero() +test from_asset_list_2() fail { + let v = from_asset_list([Pair(#"33", [])]) + v == zero() } -test without_lovelace_2() { - let v = from_lovelace(1000000) - let v2 = from_lovelace(50000000) - without_lovelace(v) == without_lovelace(v2) +test from_asset_list_3() fail { + let v = from_asset_list([Pair(#"33", [Pair(#"", 0)])]) + v != zero() } -test without_lovelace_3() { - let v = - from_asset(#"010203", #"040506", 100) - |> add(ada_policy_id, ada_asset_name, 100000000) - let v2 = from_asset(#"010203", #"040506", 100) - without_lovelace(v) == without_lovelace(v2) && without_lovelace(v) == v2 +test from_asset_list_4() { + let v = from_asset_list([Pair(#"33", [Pair(#"", 1)])]) + flatten(v) == [(#"33", #"", 1)] } -/// Negates quantities of all tokens (including Ada) in that `Value`. -/// -/// ``` -/// v1 -/// |> value.negate -/// |> value.merge(v1) -/// |> value.is_zero -/// // True -/// ``` -pub fn negate(self: Value) -> Value { - dict.map(self.inner, fn(_, a) { dict.map(a, fn(_, q) { 0 - q }) }) - |> Value +test from_asset_list_5() { + let v = from_asset_list([Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)])]) + flatten(v) == [(#"33", #"", 1), (#"33", #"33", 1)] } -/// Combine two `Value` together. -pub fn merge(left v0: Value, right v1: Value) -> Value { - Value( - dict.union_with( - v0.inner, - v1.inner, - fn(_, a0, a1) { - let result = - dict.union_with( - a0, - a1, - fn(_, q0, q1) { - let q = q0 + q1 - if q == 0 { - None - } else { - Some(q) - } - }, - ) - if dict.is_empty(result) { - None - } else { - Some(result) - } - }, - ), - ) +test from_asset_list_6() fail { + let v = + from_asset_list( + [ + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + ], + ) + v != zero() } -test merge_1() { - let v1 = from_lovelace(1) - let v2 = from_lovelace(-1) - merge(v1, v2) == zero() +test from_asset_list_7() fail { + let v = + from_asset_list( + [ + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"34", [Pair(#"", 1), Pair(#"", 1)]), + ], + ) + v != zero() } -test merge_2() { - let v1 = from_asset(#"00", #"", 1) - let v2 = from_asset(#"01", #"", 2) - let v3 = from_asset(#"02", #"", 3) +test from_asset_list_8() { let v = - from_lovelace(42) - |> merge(v3) - |> merge(v1) - |> merge(v2) + from_asset_list( + [ + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"34", [Pair(#"31", 1)]), + Pair(#"35", [Pair(#"", 1)]), + ], + ) + flatten(v) == [ + (#"33", #"", 1), + (#"33", #"33", 1), + (#"34", #"31", 1), + (#"35", #"", 1), + ] +} +test from_asset_list_9() { + let v = + from_asset_list( + [ + Pair(#"35", [Pair(#"", 1)]), + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"34", [Pair(#"31", 1)]), + ], + ) flatten(v) == [ - (#"", #"", 42), - (#"00", #"", 1), - (#"01", #"", 2), - (#"02", #"", 3), + (#"33", #"", 1), + (#"33", #"33", 1), + (#"34", #"31", 1), + (#"35", #"", 1), ] } -test merge_3() { - let v1 = from_asset(#"00", #"", 1) - let v2 = from_asset(#"00", #"", -1) - let v3 = from_asset(#"01", #"", 1) +/// Construct a `Value` from a lovelace quantity. +/// +/// Friendly reminder: 1 Ada = 1.000.000 Lovelace +pub fn from_lovelace(quantity: Int) -> Value { + from_asset(ada_policy_id, ada_asset_name, quantity) +} - let v = - zero() - |> merge(v1) - |> merge(v2) - |> merge(v3) +/// Construct an empty `Value` with nothing in it. +pub fn zero() -> Value { + Value { inner: dict.new() } +} - flatten(v) == [(#"01", #"", 1)] +// ## Inspecting + +/// Check is a `Value` is zero. That is, it has no assets and holds no Ada/Lovelace. +pub fn is_zero(self: Value) -> Bool { + self == zero() } -test merge_4() { - let v1 = from_asset(#"00", #"", 1) - let v2 = from_asset(#"00", #"", -1) +/// A specialized version of `quantity_of` for the Ada currency. +pub fn lovelace_of(self: Value) -> Int { + quantity_of(self, ada_policy_id, ada_asset_name) +} - merge(v1, v2) == zero() +/// A list of all token policies in that Value with non-zero tokens. +pub fn policies(self: Value) -> List { + dict.keys(self.inner) } -test merge_5() { - let v = - zero() - |> add(#"acab", #"beef", 0) +/// Extract the quantity of a given asset. +pub fn quantity_of( + self: Value, + policy_id: PolicyId, + asset_name: AssetName, +) -> Int { + self.inner + |> dict.get(policy_id) + |> option.and_then(dict.get(_, asset_name)) + |> option.or_else(0) +} - merge(zero(), v) == zero() +/// Get all tokens associated with a given policy. +pub fn tokens(self: Value, policy_id: PolicyId) -> Dict { + self.inner + |> dict.get(policy_id) + |> option.or_else(dict.new()) } -/// Add a (positive or negative) quantity of a single token to a value. +// ## Combining + +/// Add a (positive or negative) quantity of a single token to a assets. /// This is more efficient than [`merge`](#merge) for a single asset. pub fn add( self: Value, @@ -241,75 +266,170 @@ pub fn add( } } -test add_1() { +test add_1() { + let v = + zero() + |> add(#"acab", #"beef", 321) + |> add(#"acab", #"beef", -321) + v == zero() +} + +test add_2() { + let v = + from_lovelace(123) + |> add(#"acab", #"beef", 321) + |> add(#"acab", #"beef", -1 * 321) + v == from_lovelace(123) +} + +test add_3() { + let v = + from_lovelace(1) + |> add(ada_policy_id, ada_asset_name, 2) + |> add(ada_policy_id, ada_asset_name, 3) + v == from_lovelace(6) +} + +test add_4() { + let v = + zero() + |> add(#"acab", #"beef", 0) + v == zero() +} + +test add_5() { + let v = + zero() + |> add(#"acab", #"beef", 0) + |> add(#"acab", #"beef", 0) + v == zero() +} + +/// Combine two `Value` together. +pub fn merge(left v0: Value, right v1: Value) -> Value { + Value( + dict.union_with( + v0.inner, + v1.inner, + fn(_, a0, a1) { + let result = + dict.union_with( + a0, + a1, + fn(_, q0, q1) { + let q = q0 + q1 + if q == 0 { + None + } else { + Some(q) + } + }, + ) + if dict.is_empty(result) { + None + } else { + Some(result) + } + }, + ), + ) +} + +test merge_1() { + let v1 = from_lovelace(1) + let v2 = from_lovelace(-1) + merge(v1, v2) == zero() +} + +test merge_2() { + let v1 = from_asset(#"00", #"", 1) + let v2 = from_asset(#"01", #"", 2) + let v3 = from_asset(#"02", #"", 3) + let v = + from_lovelace(42) + |> merge(v3) + |> merge(v1) + |> merge(v2) + + flatten(v) == [ + (#"", #"", 42), + (#"00", #"", 1), + (#"01", #"", 2), + (#"02", #"", 3), + ] +} + +test merge_3() { + let v1 = from_asset(#"00", #"", 1) + let v2 = from_asset(#"00", #"", -1) + let v3 = from_asset(#"01", #"", 1) + let v = zero() - |> add(#"acab", #"beef", 321) - |> add(#"acab", #"beef", -321) - v == zero() -} + |> merge(v1) + |> merge(v2) + |> merge(v3) -test add_2() { - let v = - from_lovelace(123) - |> add(#"acab", #"beef", 321) - |> add(#"acab", #"beef", -1 * 321) - v == from_lovelace(123) + flatten(v) == [(#"01", #"", 1)] } -test add_3() { - let v = - from_lovelace(1) - |> add(ada_policy_id, ada_asset_name, 2) - |> add(ada_policy_id, ada_asset_name, 3) - v == from_lovelace(6) +test merge_4() { + let v1 = from_asset(#"00", #"", 1) + let v2 = from_asset(#"00", #"", -1) + + merge(v1, v2) == zero() } -test add_4() { +test merge_5() { let v = zero() |> add(#"acab", #"beef", 0) - v == zero() + + merge(zero(), v) == zero() } -test add_5() { - let v = - zero() - |> add(#"acab", #"beef", 0) - |> add(#"acab", #"beef", 0) - v == zero() +/// Negates quantities of all tokens (including Ada) in that `Value`. +/// +/// ``` +/// v1 +/// |> assets.negate +/// |> assets.merge(v1) +/// |> assets.is_zero +/// // True +/// ``` +pub fn negate(self: Value) -> Value { + dict.map(self.inner, fn(_, a) { dict.map(a, fn(_, q) { 0 - q }) }) + |> Value } -/// Extract the quantity of a given asset. -pub fn quantity_of( - self: Value, - policy_id: PolicyId, - asset_name: AssetName, -) -> Int { - self.inner - |> dict.get(policy_id) - |> option.and_then(dict.get(_, asset_name)) - |> option.or_else(0) +/// Get a `Value` excluding Ada. +pub fn without_lovelace(self: Value) -> Value { + dict.delete(self.inner, ada_policy_id) + |> Value } -/// A specialized version of `quantity_of` for the Ada currency. -pub fn lovelace_of(self: Value) -> Int { - quantity_of(self, ada_policy_id, ada_asset_name) +test without_lovelace_1() { + let v = from_lovelace(1000000) + without_lovelace(v) == zero() } -/// Get all tokens associated with a given policy. -pub fn tokens(self: Value, policy_id: PolicyId) -> Dict { - self.inner - |> dict.get(policy_id) - |> option.or_else(dict.new()) +test without_lovelace_2() { + let v = from_lovelace(1000000) + let v2 = from_lovelace(50000000) + without_lovelace(v) == without_lovelace(v2) } -/// A list of all token policies in that Value with non-zero tokens. -pub fn policies(self: Value) -> List { - dict.keys(self.inner) +test without_lovelace_3() { + let v = + from_asset(#"010203", #"040506", 100) + |> add(ada_policy_id, ada_asset_name, 100000000) + let v2 = from_asset(#"010203", #"040506", 100) + without_lovelace(v) == without_lovelace(v2) && without_lovelace(v) == v2 } -/// Flatten a value as list of 3-tuple (PolicyId, AssetName, Quantity). +// ## Transforming + +/// Flatten a `Value` as list of 3-tuple `(PolicyId, AssetName, Quantity)`. /// /// Handy to manipulate values as uniform lists. pub fn flatten(self: Value) -> List<(PolicyId, AssetName, Int)> { @@ -328,7 +448,7 @@ pub fn flatten(self: Value) -> List<(PolicyId, AssetName, Int)> { ) } -/// Flatten a value as a list of results, possibly discarding some along the way. +/// Flatten a `Value` as a list of results, possibly discarding some along the way. /// /// When the transform function returns `None`, the result is discarded altogether. pub fn flatten_with( @@ -380,17 +500,17 @@ test flatten_with_2() { /// Reduce a value into a single result /// /// ``` -/// value.zero() -/// |> value.add("a", "1", 10) -/// |> value.add("b", "2", 20) -/// |> value.reduce(v, 0, fn(_, _, quantity, acc) { acc + quantity }) +/// assets.zero() +/// |> assets.add("a", "1", 10) +/// |> assets.add("b", "2", 20) +/// |> assets.reduce(v, 0, fn(_, _, quantity, acc) { acc + quantity }) /// // 30 /// ``` pub fn reduce( self: Value, - start: acc, - with: fn(PolicyId, AssetName, Int, acc) -> acc, -) -> acc { + start: result, + with: fn(PolicyId, AssetName, Int, result) -> result, +) -> result { dict.foldr( self.inner, start, @@ -430,261 +550,7 @@ test reduce_3() { result == 1 } -/// Promote an arbitrary list of assets into a `Value`. This function fails -/// (i.e. halt the program execution) if: -/// -/// - there's any duplicate amongst `PolicyId`; -/// - there's any duplicate amongst `AssetName`; -/// - the `AssetName` aren't sorted in ascending lexicographic order; or -/// - any asset quantity is null. -/// -/// This function is meant to turn arbitrary user-defined `Data` into safe `Value`, -/// while checking for internal invariants. -pub fn from_asset_list(xs: Pairs>) -> Value { - xs - |> list.foldr( - dict.new(), - fn(inner, acc) { - expect Pair(p, [_, ..] as x) = inner - x - |> from_ascending_pairs_with(fn(v) { v != 0 }) - |> dict.insert_with( - acc, - p, - _, - fn(_, _, _) { - fail @"Duplicate policy in the asset list." - }, - ) - }, - ) - |> Value -} - -test from_asset_list_1() { - let v = from_asset_list([]) - v == zero() -} - -test from_asset_list_2() fail { - let v = from_asset_list([Pair(#"33", [])]) - v == zero() -} - -test from_asset_list_3() fail { - let v = from_asset_list([Pair(#"33", [Pair(#"", 0)])]) - v != zero() -} - -test from_asset_list_4() { - let v = from_asset_list([Pair(#"33", [Pair(#"", 1)])]) - flatten(v) == [(#"33", #"", 1)] -} - -test from_asset_list_5() { - let v = from_asset_list([Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)])]) - flatten(v) == [(#"33", #"", 1), (#"33", #"33", 1)] -} - -test from_asset_list_6() fail { - let v = - from_asset_list( - [ - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - ], - ) - v != zero() -} - -test from_asset_list_7() fail { - let v = - from_asset_list( - [ - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"34", [Pair(#"", 1), Pair(#"", 1)]), - ], - ) - v != zero() -} - -test from_asset_list_8() { - let v = - from_asset_list( - [ - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"34", [Pair(#"31", 1)]), - Pair(#"35", [Pair(#"", 1)]), - ], - ) - flatten(v) == [ - (#"33", #"", 1), - (#"33", #"33", 1), - (#"34", #"31", 1), - (#"35", #"", 1), - ] -} - -test from_asset_list_9() { - let v = - from_asset_list( - [ - Pair(#"35", [Pair(#"", 1)]), - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"34", [Pair(#"31", 1)]), - ], - ) - flatten(v) == [ - (#"33", #"", 1), - (#"33", #"33", 1), - (#"34", #"31", 1), - (#"35", #"", 1), - ] -} - /// Convert the value into a dictionary of dictionaries. pub fn to_dict(self: Value) -> Dict> { self.inner } - -/// A multi-asset value that can be found when minting transaction. It always holds -/// a null quantity of _Ada_. Note that because of historical reasons, this is slightly -/// different from `Value` found in transaction outputs. -/// -/// Note that you're never expected to construct a `MintedValue` yourself. If you need to -/// manipulate multi-asset values, use [Value](#Value) -/// -/// See also [`from_minted_value`](#from_minted_value). -pub opaque type MintedValue { - inner: Dict>, -} - -/// Convert minted value into a dictionary of dictionaries. -pub fn minted_to_dict(self: MintedValue) -> Dict> { - self.inner -} - -/// Convert a [`MintedValue`](#MintedValue) into a [`Value`](#Value). -pub fn from_minted_value(self: MintedValue) -> Value { - self.inner |> dict.delete(ada_policy_id) |> Value -} - -test from_minted_value_1() { - flatten(from_minted_value(from_internal_list([]))) == [] -} - -test from_minted_value_2() { - flatten(from_minted_value(from_internal_list([("p0", "a0", 1)]))) == [ - ("p0", "a0", 1), - ] -} - -test from_minted_value_3() { - let assets = - [("p0", "a0", 1), ("p1", "a0", 1), ("p0", "a0", 1), ("p1", "a1", 1)] - - let result = - [("p0", "a0", 2), ("p1", "a0", 1), ("p1", "a1", 1)] - - flatten(from_minted_value(from_internal_list(assets))) == result -} - -test from_minted_value_4() { - let assets = - [ - ("", "", 0), - ("p0", "a0", 1), - ("p1", "a0", 1), - ("p0", "a0", 1), - ("p1", "a1", 1), - ] - - let result = - [("p0", "a0", 2), ("p1", "a0", 1), ("p1", "a1", 1)] - - flatten(from_minted_value(from_internal_list(assets))) == result -} - -test from_minted_value_5() { - let assets = - [ - ("p0", "a0", 1), - ("p0", "a1", 1), - ("p1", "a0", 1), - ("p1", "a1", 1), - ("p1", "a2", 1), - ("p2", "a0", 1), - ("p2", "a1", 1), - ("p3", "a0", 1), - ("p3", "a1", 1), - ("p3", "a2", 1), - ("p3", "a3", 1), - ("p3", "a4", 1), - ("p3", "a5", 1), - ("p3", "a6", 1), - ("p3", "a7", 1), - ] - - flatten(from_minted_value(from_internal_list(assets))) == assets -} - -/// Convert a [`Value`](#Value) into a [`MintedValue`](#MintedValue). -pub fn to_minted_value(self: Value) -> MintedValue { - self.inner - |> dict.insert(ada_policy_id, dict.insert(dict.new(), ada_asset_name, 0)) - |> MintedValue -} - -test to_minted_value_1() { - let minted_value = to_minted_value(zero()) - ( minted_value.inner |> dict.to_pairs |> list.length ) == 1 -} - -test to_minted_value_2() { - let minted_value = to_minted_value(from_lovelace(42)) - ( - minted_value.inner - |> dict.get(ada_policy_id) - |> option.and_then(dict.get(_, ada_asset_name)) - ) == Some(0) -} - -/// Convert a list of tokens into a `MintedValue`. -/// -/// NOTE: Not exposed because we do not want people to construct `MintedValue`. Only -/// get them from the script context. -fn from_internal_list(xs: List<(PolicyId, AssetName, Int)>) -> MintedValue { - list.foldr( - xs, - MintedValue(dict.new()), - fn(elem, st) { - let (policy_id, asset_name, quantity) = elem - unchecked_add(st, policy_id, asset_name, quantity) - }, - ) -} - -fn unchecked_add( - self: MintedValue, - policy_id: PolicyId, - asset_name: AssetName, - quantity: Int, -) -> MintedValue { - MintedValue( - dict.insert_with( - self.inner, - policy_id, - dict.from_ascending_pairs([Pair(asset_name, quantity)]), - fn(_, left, _right) { - Some( - dict.insert_with( - left, - asset_name, - quantity, - fn(_k, ql, qr) { Some(ql + qr) }, - ), - ) - }, - ), - ) -} diff --git a/lib/cardano/certificate.ak b/lib/cardano/certificate.ak index 6ca9045..c8ee231 100644 --- a/lib/cardano/certificate.ak +++ b/lib/cardano/certificate.ak @@ -1,60 +1,82 @@ +use aiken/crypto.{Blake2b_224, Hash, VerificationKey, VerificationKeyHash} +use cardano/address.{Credential} use cardano/assets.{Lovelace} -use cardano/credential.{Credential, StakePoolId, VerificationKeyHash} + +pub type StakePoolId = + Hash /// An on-chain certificate attesting of some operation. Publishing /// certificates / triggers different kind of rules; most of the time, /// they require signatures from / specific keys. pub type Certificate { - // Register a stake credential with an optional deposit amount. - // The deposit is always present when using the new registration certificate - // format available since the Conway era. - RegisterCredential { credential: Credential, deposit: Option } - // Un-Register a stake credential with an optional refund amount - // The deposit is always present when using the new de-registration certificate - // format available since the Conway era. - UnregisterCredential { credential: Credential, refund: Option } - // Delegate stake to a [Delegate](#Delegate). + /// Register a stake credential with an optional deposit amount. + /// The deposit is always present when using the new registration certificate + /// format available since the Conway era. + RegisterCredential { + credential: Credential, + /// > [!INFO] + /// > The `deposit` ought to be an `Option`, but due to unfortunate + /// > circumstances it will always be instantiated to `None` even when set in + /// > the host transaction. This is what the `Never` type captures here. + deposit: Never, + } + /// Un-Register a stake credential with an optional refund amount + /// The deposit is always present when using the new de-registration certificate + /// format available since the Conway era. + UnregisterCredential { + credential: Credential, + /// > [!INFO] + /// > The `refund` ought to be an `Option`, but due to unfortunate + /// > circumstances it will always be instantiated to `None` even when set in + /// > the host transaction. This is what the `Never` type captures here. + refund: Never, + } + /// Delegate stake to a [Delegate](#Delegate). DelegateCredential { credential: Credential, delegate: Delegate } - // Register and delegate staking credential to a Delegatee in one certificate. + /// Register and delegate staking credential to a Delegatee in one certificate. RegisterAndDelegateCredential { credential: Credential, delegate: Delegate, deposit: Lovelace, } - // Register a delegate representative (a.k.a DRep). The deposit is explicit and - // is refunded when the delegate steps down (unregister). + /// Register a delegate representative (a.k.a DRep). The deposit is explicit and + /// is refunded when the delegate steps down (unregister). RegisterDelegateRepresentative { delegate_representative: Credential, deposit: Lovelace, } - // Update a delegate representative (a.k.a DRep). The certificate also contains - // metadata which aren't visible on-chain. + /// Update a delegate representative (a.k.a DRep). The certificate also contains + /// metadata which aren't visible on-chain. UpdateDelegateRepresentative { delegate_representative: Credential } - // UnRegister a delegate representative, and refund back its past deposit. + /// UnRegister a delegate representative, and refund back its past deposit. UnregisterDelegateRepresentative { delegate_representative: Credential, refund: Lovelace, } - // Register a new stake pool + /// Register a new stake pool RegisterStakePool { - // The hash digest of the stake pool's cold (public) key + /// The hash digest of the stake pool's cold (public) key stake_pool: StakePoolId, - // The hash digest of the stake pool's VRF (public) key + /// The hash digest of the stake pool's VRF (public) key vrf: VerificationKeyHash, } - // Retire a stake pool. 'at_epoch' indicates in which the retirement will take place + /// Retire a stake pool. 'at_epoch' indicates in which the retirement will take place RetireStakePool { stake_pool: StakePoolId, at_epoch: Int } - // Authorize a Hot credential for a specific Committee member's cold credential + /// Authorize a Hot credential for a specific Committee member's cold credential AuthorizeConstitutionalCommitteeProxy { constitutional_committee_member: Credential, proxy: Credential, } - // Step down from the constitutional committee as a member. + /// Step down from the constitutional committee as a member. RetireFromConstitutionalCommittee { constitutional_committee_member: Credential, } } +/// A type of stake delegation that can be either block-production, vote or +/// both. Note that delegation types aren't cancelling one another, so it is +/// possible to delegate block production in one transaction, and delegate vote +/// in another. This second delegation **does NOT** invalidate the first one. pub type Delegate { DelegateBlockProduction { stake_pool: StakePoolId } DelegateVote { delegate_representative: DelegateRepresentative } diff --git a/lib/cardano/governance.ak b/lib/cardano/governance.ak index b01d81e..3ec9680 100644 --- a/lib/cardano/governance.ak +++ b/lib/cardano/governance.ak @@ -1,8 +1,8 @@ use aiken/collection.{Index} -use aiken/hash.{Blake2b_256, Hash} +use aiken/crypto.{Blake2b_256, Hash, ScriptHash, VerificationKeyHash} use aiken/math/rational.{Rational} +use cardano/address.{Credential} use cardano/assets.{Lovelace} -use cardano/credential.{Credential, ScriptHash, VerificationKeyHash} use cardano/governance/protocol_parameters.{ProtocolParametersUpdate} pub type ProposalProcedure { diff --git a/lib/cardano/governance/protocol_parameters.ak b/lib/cardano/governance/protocol_parameters.ak index ca2c575..27e4dbc 100644 --- a/lib/cardano/governance/protocol_parameters.ak +++ b/lib/cardano/governance/protocol_parameters.ak @@ -332,7 +332,6 @@ fn into_spo_voting_thresholds(param: Data) -> StakePoolOperatorVotingThresholds fn into_drep_voting_thresholds( param: Data, ) -> DelegateRepresentativeVotingThresholds { - trace @"drep voting thresholds": param expect [ motion_of_no_confidence, constitutional_committee, diff --git a/lib/cardano/script_context.ak b/lib/cardano/script_context.ak new file mode 100644 index 0000000..ff73836 --- /dev/null +++ b/lib/cardano/script_context.ak @@ -0,0 +1,62 @@ +//// This module contains utilities for manually dealing with [`ScriptContext`](#ScriptContext). This is only ever useful for writing custom `else` handlers in validators. +//// +//// > [!NOTE] +//// > Unless you know what you're doing, you should prefer using named handlers: +//// > +//// > - `mint` +//// > - `spend` +//// > - `withdraw` +//// > - `publish` +//// > - `vote` +//// > - `propose` + +use aiken/collection.{Index} +use cardano/address.{Credential} +use cardano/assets.{PolicyId} +use cardano/certificate.{Certificate} +use cardano/governance.{ProposalProcedure, Voter} +use cardano/transaction.{OutputReference, Redeemer, Transaction} + +/// A context given to a script by the Cardano ledger when being executed. +/// +/// The context contains information about the entire transaction that contains +/// the script. The transaction may also contain other scripts; to distinguish +/// between multiple scripts, the [`ScriptContext`](#ScriptContext) contains a +/// [`ScriptInfo`](#ScriptInfo) which indicates which script (or, for what +/// purpose) the transaction is being executed. +pub type ScriptContext { + transaction: Transaction, + redeemer: Redeemer, + info: ScriptInfo, +} + +/// Characterizes the script information. The main (and only) difference with [`ScriptPurpose`](./transaction.html#ScriptPurpose) resides in the `Spending` variant which here contains a second field `datum: Option`. +pub type ScriptInfo { + /// For scripts executed as minting/burning policies, to insert + /// or remove assets from circulation. It's parameterized by the identifier + /// of the associated policy. + Minting(PolicyId) + /// For scripts that are used as payment credentials for addresses in + /// transaction outputs. They govern the rule by which the output they + /// reference can be spent. + Spending { output: OutputReference, datum: Option } + /// For scripts that validate reward withdrawals from a reward account. + /// + /// The argument identifies the target reward account. + Withdrawing(Credential) + /// Needed when delegating to a pool using stake credentials defined as a + /// custom script. This purpose is also triggered when de-registering such + /// stake credentials. + /// + /// The Int is a 0-based index of the given `Certificate` in `certificates`. + Publishing { at: Index, certificate: Certificate } + /// Voting for a type of voter using a governance action id to vote + /// yes / no / abstain inside a transaction. + /// + /// The voter is who is doing the governance action. + Voting(Voter) + /// Used to propose a governance action. + /// + /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. + Proposing { at: Index, proposal_procedure: ProposalProcedure } +} diff --git a/lib/cardano/transaction.ak b/lib/cardano/transaction.ak index 9c152f1..7cb4b70 100644 --- a/lib/cardano/transaction.ak +++ b/lib/cardano/transaction.ak @@ -2,64 +2,19 @@ use aiken/builtin use aiken/collection.{Index} use aiken/collection/dict.{Dict} use aiken/collection/list -use aiken/hash.{Blake2b_256, Hash, blake2b_256} +use aiken/crypto.{ + Blake2b_256, DataHash, Hash, ScriptHash, VerificationKeyHash, blake2b_256, +} use aiken/interval.{Interval} use aiken/option +use cardano/address.{Address, Credential, Script, VerificationKey} use cardano/assets.{Lovelace, PolicyId, Value} use cardano/certificate.{Certificate} -use cardano/credential.{ - Address, Credential, DatumHash, Script, ScriptHash, VerificationKey, - VerificationKeyHash, -} use cardano/governance.{GovernanceActionId, ProposalProcedure, Vote, Voter} pub type TransactionId = Hash -/// A context given to a script by the Cardano ledger when being executed. -/// -/// The context contains information about the entire transaction that contains -/// the script. The transaction may also contain other scripts; to distinguish -/// between multiple scripts, the `ScriptInfo` contains a `purpose` -/// which indicates which script (or, for what purpose) of the transaction is -/// being executed. -pub type ScriptContext { - transaction: Transaction, - redeemer: Redeemer, - info: ScriptInfo, -} - -/// Characterizes the script information. -pub type ScriptInfo { - /// For scripts executed as minting/burning policies, to insert - /// or remove assets from circulation. It's parameterized by the identifier - /// of the associated policy. - Minting(PolicyId) - /// For scripts that are used as payment credentials for addresses in - /// transaction outputs. They govern the rule by which the output they - /// reference can be spent. - Spending(OutputReference, Option) - /// For scripts that validate reward withdrawals from a reward account. - /// - /// The argument identifies the target reward account. - Withdrawing(Credential) - /// Needed when delegating to a pool using stake credentials defined as a - /// Plutus script. This purpose is also triggered when de-registering such - /// stake credentials. - /// - /// The Int is a 0-based index of the given `TxCert` in `certificates`. - Publishing(Index, Certificate) - /// Voting for a type of voter using a governance action id to vote - /// yes / no / abstain inside a transaction. - /// - /// The voter is who is doing the governance action. - Voting(Voter) - /// Used to propose a governance action. - /// - /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. - Proposing(Index, ProposalProcedure) -} - /// Characterizes the script purpose. pub type ScriptPurpose { /// For scripts executed as minting/burning policies, to insert @@ -75,11 +30,11 @@ pub type ScriptPurpose { /// The argument identifies the target reward account. Withdraw(Credential) /// Needed when delegating to a pool using stake credentials defined as a - /// Plutus script. This purpose is also triggered when de-registering such + /// custom script. This purpose is also triggered when de-registering such /// stake credentials. /// - /// The Int is a 0-based index of the given `TxCert` in `certificates`. - Publish { certificate_index: Int, certificate: Certificate } + /// The Int is a 0-based index of the given `Certificate` in `certificates`. + Publishing { at: Index, certificate: Certificate } /// Voting for a type of voter using a governance action id to vote /// yes / no / abstain inside a transaction. /// @@ -88,14 +43,14 @@ pub type ScriptPurpose { /// Used to propose a governance action. /// /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. - Propose(Int, ProposalProcedure) + Proposing { at: Index, proposal_procedure: ProposalProcedure } } -/// A Cardano `Transaction`, as seen by Plutus scripts. +/// A Cardano `Transaction`, as seen by on-chain scripts. /// /// Note that this is a representation of a transaction, and not the 1:1 /// translation of the transaction as seen by the ledger. In particular, -/// Plutus scripts can't see inputs locked by bootstrap addresses, outputs +/// on-chain scripts can't see inputs locked by bootstrap addresses, outputs /// to bootstrap addresses or just transaction metadata. pub type Transaction { inputs: List, @@ -104,64 +59,27 @@ pub type Transaction { fee: Lovelace, mint: Value, certificates: List, - /// ⚠️ | Withdrawals are ordered by ascending [Credential](./credential.html#Credential). Yet, note that script credentials are treated as lower values than verification key credentials. - /// --- | --- + /// > [!IMPORTANT] + /// > Withdrawals are ordered by ascending [Credential](./credential.html#Credential). Yet, note that [`Script`](./credential.html#Credential) credentials are treated as **lower values** than [`VerificationKey`](./credential.html#Credential) credentials. withdrawals: Pairs, validity_range: ValidityRange, extra_signatories: List, - /// ⚠️ | Redeemers are ordered by ascending [ScriptPurpose](./governance.html#ScriptPurpose). + /// > [!IMPORTANT] + /// > Redeemers are ordered by ascending [ScriptPurpose](./transaction.html#ScriptPurpose). redeemers: Pairs, - datums: Dict, + datums: Dict, id: TransactionId, - /// ⚠️ | Votes are ordered by ascending [Voter](./governance.html#Voter) and [GovernanceActionId](./governance.html#GovernanceActionId).
First constructor variants in a type are treated as lower indices; except for [Credential](./credential.html#Credential) where stake credentials are treated as lower values than verification key credentials. - /// --- | --- + /// > [!IMPORTANT] + /// > Votes are ordered by ascending [Voter](./governance.html#Voter) and [GovernanceActionId](./governance.html#GovernanceActionId).
First constructor variants in a type are treated as lower indices; except for [Credential](./credential.html#Credential) where [`Script`](./credential.html#Credential) credentials are treated as **lower values** than [`VerificationKey`](./credential.html#Credential) credentials. votes: Pairs>, proposal_procedures: List, current_treasury_amount: Option, treasury_donation: Option, } -/// A placeholder / empty `Transaction` to serve as a base in a transaction -/// builder. This is particularly useful for constructing test transactions. -/// -/// Every field is empty or null, and we have in particular: -/// -/// ```aiken -/// use aiken/transaction -/// -/// transaction.placeholder().id == -/// #"0000000000000000000000000000000000000000000000000000000000000000" -/// -/// transaction.placeholder().validity_range == interval.everything() -/// ``` -pub fn placeholder() -> Transaction { - Transaction { - inputs: [], - reference_inputs: [], - outputs: [], - fee: 0, - mint: assets.zero(), - certificates: [], - withdrawals: [], - validity_range: interval.everything(), - extra_signatories: [], - redeemers: [], - datums: dict.new(), - id: #"0000000000000000000000000000000000000000000000000000000000000000", - votes: [], - proposal_procedures: [], - current_treasury_amount: None, - treasury_donation: None, - } -} - -/// A number of milliseconds since 00:00:00 UTC on 1 January 1970. -pub type PosixTime = - Int - -/// An interval of POSIX time, measured in number milliseconds since 1970-01-01T00:00:00Z. +/// An interval of POSIX time, measured in **number of milliseconds** since 1970-01-01T00:00:00Z. pub type ValidityRange = - Interval + Interval /// An `Input` made of an output reference and, the resolved value associated with that output. pub type Input { @@ -189,7 +107,7 @@ pub type Output { pub type Datum { NoDatum /// A datum referenced by its hash digest. - DatumHash(DatumHash) + DatumHash(DataHash) /// A datum completely inlined in the output. InlineDatum(Data) } @@ -200,18 +118,17 @@ pub type Datum { pub type Redeemer = Data +// ## Querying + /// Find an input by its [`OutputReference`](#OutputReference). This is typically used in /// combination with the `Spend` [`ScriptPurpose`](#ScriptPurpose) to find a script's own /// input. /// /// ```aiken /// validator { -/// fn(datum, redeemer, ctx: ScriptContext) { -/// expect Spend(my_output_reference) = -/// ctx.purpose -/// +/// spend(datum, redeemer, my_output_reference, self) { /// expect Some(input) = -/// ctx.transaction.inputs +/// self.inputs /// |> transaction.find_input(my_output_reference) /// } /// } @@ -229,8 +146,8 @@ pub fn find_input( /// witnesses. pub fn find_datum( outputs: List, - datums: Dict, - datum_hash: DatumHash, + datums: Dict, + datum_hash: DataHash, ) -> Option { datums |> dict.get(datum_hash) @@ -273,3 +190,39 @@ pub fn find_script_outputs( }, ) } + +// ## Testing + +/// A placeholder / empty `Transaction` to serve as a base in a transaction +/// builder. This is particularly useful for constructing test transactions. +/// +/// Every field is empty or null, and we have in particular: +/// +/// ```aiken +/// use aiken/interval +/// +/// transaction.placeholder().id == +/// #"0000000000000000000000000000000000000000000000000000000000000000" +/// +/// transaction.placeholder().validity_range == interval.everything() +/// ``` +pub fn placeholder() -> Transaction { + Transaction { + inputs: [], + reference_inputs: [], + outputs: [], + fee: 0, + mint: assets.zero(), + certificates: [], + withdrawals: [], + validity_range: interval.everything(), + extra_signatories: [], + redeemers: [], + datums: dict.new(), + id: #"0000000000000000000000000000000000000000000000000000000000000000", + votes: [], + proposal_procedures: [], + current_treasury_amount: None, + treasury_donation: None, + } +}