diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..8913685
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,20 @@
+name: Tests
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: aiken-lang/setup-aiken@v0.1.0
+ with:
+ version: v1
+
+ - run: aiken fmt --check
+ - run: aiken check -D
+ - run: aiken build
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ff7811b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+# Aiken compilation artifacts
+artifacts/
+# Aiken's project working directory
+build/
+# Aiken's default documentation export
+docs/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fb0a00a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+# Bullet
+
+Write validators in the `validators` folder, and supporting functions in the
+`lib` folder using `.ak` as a file extension.
+
+For example, as `validators/always_true.ak`
+
+```gleam
+validator {
+ fn spend(_datum: Data, _redeemer: Data, _context: Data) -> Bool {
+ True
+ }
+}
+```
+
+## Building
+
+```sh
+aiken build
+```
+
+## Testing
+
+You can write tests in any module using the `test` keyword. For example:
+
+```gleam
+test foo() {
+ 1 + 1 == 2
+}
+```
+
+To run all tests, simply do:
+
+```sh
+aiken check
+```
+
+To run only tests matching the string `foo`, do:
+
+```sh
+aiken check -m foo
+```
+
+## Documentation
+
+If you're writing a library, you might want to generate an HTML documentation
+for it.
+
+Use:
+
+```sh
+aiken docs
+```
+
+## Resources
+
+Find more on the [Aiken's user manual](https://aiken-lang.org).
diff --git a/aiken.lock b/aiken.lock
new file mode 100644
index 0000000..0a72eb3
--- /dev/null
+++ b/aiken.lock
@@ -0,0 +1,15 @@
+# This file was generated by Aiken
+# You typically do not need to edit this file
+
+[[requirements]]
+name = "aiken-lang/stdlib"
+version = "1.7.0"
+source = "github"
+
+[[packages]]
+name = "aiken-lang/stdlib"
+version = "1.7.0"
+requirements = []
+source = "github"
+
+[etags]
diff --git a/aiken.toml b/aiken.toml
new file mode 100644
index 0000000..8abb7ed
--- /dev/null
+++ b/aiken.toml
@@ -0,0 +1,14 @@
+name = "aiken-lang/composable-account"
+version = "0.0.0"
+license = "Apache-2.0"
+description = "Aiken contracts for project 'aiken-lang/composable-account'"
+
+[repository]
+user = "aiken-lang"
+project = "composable-account"
+platform = "github"
+
+[[dependencies]]
+name = "aiken-lang/stdlib"
+version = "1.7.0"
+source = "github"
diff --git a/lib/composable-account/intentions.ak b/lib/composable-account/intentions.ak
new file mode 100644
index 0000000..e69de29
diff --git a/lib/composable-account/utils.ak b/lib/composable-account/utils.ak
new file mode 100644
index 0000000..aa9db35
--- /dev/null
+++ b/lib/composable-account/utils.ak
@@ -0,0 +1,38 @@
+use aiken/builtin
+use aiken/dict.{Dict}
+use aiken/hash.{Blake2b_224, Hash}
+use aiken/list
+use aiken/transaction/credential.{VerificationKey}
+
+pub fn list_at(list: List, index: Int) -> a {
+ if index == 0 {
+ builtin.head_list(list)
+ } else {
+ builtin.tail_list(list) |> list_at(index - 1)
+ }
+}
+
+pub fn dict_get(dict: Dict, key: a) -> b {
+ dict |> dict.to_list |> do_dict_get(key)
+}
+
+fn do_dict_get(list: List<(a, b)>, key: a) -> b {
+ expect [head, ..tail] = list
+
+ if builtin.fst_pair(head) == key {
+ builtin.snd_pair(head)
+ } else {
+ do_dict_get(tail, key)
+ }
+}
+
+pub fn validate_keys_present(
+ owners: List>,
+ signers: List>,
+) -> Bool {
+ when owners is {
+ [] -> True
+ [owner, ..rest_owners] ->
+ list.has(signers, owner) && validate_keys_present(rest_owners, signers)
+ }
+}
diff --git a/validators/account.ak b/validators/account.ak
new file mode 100644
index 0000000..f63053c
--- /dev/null
+++ b/validators/account.ak
@@ -0,0 +1,251 @@
+use aiken/builtin
+use aiken/dict.{Dict}
+use aiken/hash.{Blake2b_224, Hash}
+use aiken/interval.{Finite, Interval, IntervalBound}
+use aiken/list
+use aiken/time.{PosixTime}
+use aiken/transaction.{
+ DatumHash, InlineDatum, Input, Mint, Output, OutputReference, Publish,
+ Redeemer, ScriptContext, ScriptPurpose, Spend, Transaction, WithdrawFrom,
+}
+use aiken/transaction/certificate.{
+ CredentialDelegation, CredentialDeregistration,
+}
+use aiken/transaction/credential.{
+ Address, Credential, Inline, PoolId, ScriptCredential, VerificationKey,
+}
+use aiken/transaction/value.{
+ AssetName, PolicyId, from_minted_value, quantity_of, tokens,
+}
+use composable_account/utils.{list_at, validate_keys_present}
+
+// For now only support Pub key hash
+type Owner {
+ owner_list: List>,
+}
+
+type OutputCondition {
+ HasAddress(Address)
+ HasToken(PolicyId, AssetName, Int)
+ HasDatum(Data)
+ HasPC(Credential)
+ HasField { field_data: Data, field_path: List }
+ HasHash(ByteArray)
+}
+
+type TxCondition {
+ Mints(PolicyId, AssetName, Int)
+ IsAfter(PosixTime)
+ IsBefore(PosixTime)
+ /// Please note the `outer list` in InputsCondition could all be satisfied by a single input.
+ /// The checks are per all inputs not a unique input
+ /// The `inner list` acts on a single input ensuring the input meets all conditions via &&
+ InputsCondition(List>)
+ ReferenceInputsCondition(List>)
+ OutputsCondition(List>)
+ StakesTo(PoolId)
+}
+
+type IntentionCondition {
+ Is(TxCondition)
+ And(List)
+ Or(List)
+ Not(TxCondition)
+}
+
+type State {
+ ControlState { owner: Owner }
+ NonceState
+}
+
+type Intention {
+ condition: IntentionCondition,
+ value_leaving: List<(PolicyId, AssetName, Int)>,
+ nonce: OutputReference,
+}
+
+type DataEnvelope {
+ prefix: ByteArray,
+ intention: Intention,
+ postfix: ByteArray,
+ signatures: List,
+}
+
+type ValidateRedeemer {
+ SpendFromAccount(DataEnvelope)
+ UpdateConfig(Int, Int)
+ Destroy(Int)
+ Initialize(Int)
+}
+
+type StakeRedeemer {
+ InitAddress(ByteArray)
+ StakeTo
+ Withdraw
+}
+
+validator(init_input: OutputReference) {
+ fn stake_validator(_rdr, ctx: ScriptContext) -> Bool {
+ todo
+ }
+}
+
+validator {
+ fn spend(_dat, input_index: Int, ctx: ScriptContext) -> Bool {
+ expect ScriptContext { transaction: tx, purpose: Spend(own_ref) } = ctx
+
+ let Transaction { inputs, withdrawals, .. } = tx
+
+ let Input { output_reference, output } = inputs |> list_at(input_index)
+
+ let withdraw_cred = Inline(output.address.payment_credential)
+
+ // Using index for faster look up and this check is safe as long as the
+ // withdraw credential is aware of this input
+ output_reference == own_ref && dict.has_key(withdrawals, withdraw_cred)
+ }
+
+ fn validate(rdr: Data, ctx: ScriptContext) -> Bool {
+ let ScriptContext { transaction: tx, purpose } = ctx
+
+ let Transaction {
+ inputs,
+ reference_inputs,
+ outputs,
+ mint,
+ redeemers,
+ extra_signatories,
+ ..
+ } = tx
+
+ when purpose is {
+ WithdrawFrom(Inline(ScriptCredential(own_policy_id))) -> {
+ expect rdr: ValidateRedeemer = rdr
+
+ when rdr is {
+ SpendFromAccount(data_envelope) -> todo
+ UpdateConfig(input_index, output_index) -> {
+ let Output { address, value, datum, .. } =
+ list_at(inputs, input_index).output
+
+ expect [(asset_name, 1)] =
+ value |> value.tokens(own_policy_id) |> dict.to_list
+
+ expect InlineDatum(datum) = datum
+
+ expect ControlState { owner }: State = datum
+
+ let Output {
+ address: out_address,
+ value: out_value,
+ datum: out_datum,
+ ..
+ } = list_at(outputs, output_index)
+
+ expect InlineDatum(out_datum) = out_datum
+
+ expect ControlState { .. }: State = out_datum
+
+ expect [(out_asset_name, 1)] =
+ out_value |> value.tokens(own_policy_id) |> dict.to_list
+
+ and {
+ validate_keys_present(owner.owner_list, extra_signatories),
+ asset_name == out_asset_name,
+ address == out_address,
+ }
+ }
+ Destroy(input_index) -> {
+ let Output { value, datum, .. } =
+ list_at(inputs, input_index).output
+
+ expect [(asset_name, 1)] =
+ value |> value.tokens(own_policy_id) |> dict.to_list
+
+ let burned_quantity =
+ mint
+ |> value.from_minted_value
+ |> quantity_of(own_policy_id, asset_name)
+
+ expect InlineDatum(datum) = datum
+
+ expect ControlState { owner }: State = datum
+
+ and {
+ burned_quantity == -1,
+ validate_keys_present(owner.owner_list, extra_signatories),
+ }
+ }
+
+ _ -> False
+ }
+ }
+
+ Mint(own_policy_id) -> {
+ expect [(token_name, amount)] =
+ mint
+ |> value.from_minted_value
+ |> value.tokens(own_policy_id)
+ |> dict.to_list
+
+ if amount == 1 {
+ expect Initialize(output_index): ValidateRedeemer = rdr
+
+ let control_output = list_at(outputs, output_index)
+
+ validate_minted_nft(
+ control_output,
+ own_policy_id,
+ token_name,
+ redeemers,
+ )
+ } else if amount == -1 {
+ True
+ } else {
+ False
+ }
+ }
+
+ _ -> False
+ }
+ }
+}
+
+/// Used to validate initializing the account.
+/// This depends on the stake_validator to guarantee address to NFT uniqueness
+fn validate_minted_nft(
+ output: Output,
+ own_policy_id: PolicyId,
+ token_name: AssetName,
+ redeemers: Dict,
+) -> Bool {
+ expect Output {
+ address: Address {
+ payment_credential: ScriptCredential(own_pc),
+ stake_credential,
+ },
+ value,
+ datum,
+ ..
+ } = output
+
+ let asset_quantity = quantity_of(value, own_policy_id, token_name)
+
+ expect Some(withdraw_cred) = stake_credential
+
+ let withdraw_purpose = WithdrawFrom(withdraw_cred)
+
+ expect Some(stake_redeemer) = dict.get(redeemers, withdraw_purpose)
+
+ let expected_redeemer: Data = InitAddress(token_name)
+
+ expect InlineDatum(datum) = datum
+
+ expect ControlState { .. }: State = datum
+
+ and {
+ own_pc == own_policy_id,
+ stake_redeemer == expected_redeemer,
+ asset_quantity == 1,
+ }
+}