diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b4ce3fa..f615d67 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,6 +4,7 @@ on: [push, pull_request]
env:
STARKNET_FOUNDRY_VERSION: 0.12.0
+ STARKNET_FOUNDRY_VERSION: 0.12.0
jobs:
check:
@@ -16,9 +17,8 @@ jobs:
- name: Install starknet foundry
run: |
curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh -s -- -v ${STARKNET_FOUNDRY_VERSION}
- echo "/root/.local/bin" >> $GITHUB_PATH
- echo "Listing /root/.local/bin:"
- ls -al /root/.local/bin
+ echo "/home/runner/.local/bin" >> $GITHUB_PATH
+
- name: Run cairo tests
run: |
export PATH="/root/.local/bin:$PATH"
diff --git a/.gitignore b/.gitignore
index f895146..8b89b5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
# will have compiled files and executables
debug/
target/
-
+node_modules/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
diff --git a/Scarb.lock b/Scarb.lock
index 453d3ed..1f46e42 100644
--- a/Scarb.lock
+++ b/Scarb.lock
@@ -1,6 +1,11 @@
# Code generated by scarb DO NOT EDIT.
version = 1
+[[package]]
+name = "openzeppelin"
+version = "0.8.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.8.0#c23e8e96de60e6e3159b1ff8591a1187269c0eb7"
+
[[package]]
name = "snforge_std"
version = "0.1.0"
@@ -10,5 +15,6 @@ source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.12.0#0c3
name = "tokei"
version = "0.1.0"
dependencies = [
+ "openzeppelin",
"snforge_std",
]
diff --git a/Scarb.toml b/Scarb.toml
index 4366feb..692c51c 100644
--- a/Scarb.toml
+++ b/Scarb.toml
@@ -20,6 +20,7 @@ sierra-replace-ids = true
[dependencies]
starknet = ">=2.3.1"
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.12.0" }
+openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.0" }
[tool.snforge]
diff --git a/book/src/README.md b/book/src/README.md
index fe1d7da..0efb8b4 100644
--- a/book/src/README.md
+++ b/book/src/README.md
@@ -5,3 +5,43 @@ Tokei is a token streaming protocol, inspired by Sablier, for Starknet.
Tokei means "clock" in Japanese.
![Tokei](./assets/tokei-hello.gif)
+
+# The Advent of Tokei: A New Era in Token Streaming
+
+Token streaming, at its core, is the continuous transfer of assets over time. Tokei takes this concept further, offering unmatched flexibility and customizability within the Starknet ecosystem. This enables users to create bespoke streams tailored to a wide range of financial scenarios.
+
+## Distinctive Features of Tokei
+
+- **Linear Lockup Streams:** Tokeiโs hallmark feature, these streams are crafted for a gradual and controlled fund release, perfectly suited for structured payment plans and corporate vesting schedules.
+- **Customization at Its Core:** Users have the liberty to adjust parameters like start and end times, cliff durations, and can opt for stream cancelability and transferability, making Tokei versatile for various streaming needs.
+- **Integration with ERC Standards:** Supporting ERC-20 tokens for streaming transactions and using ERC-721 (NFTs) to denote stream ownership, Tokei broadens its reach across different asset classes.
+- **Transparent Fee Structure:** The protocol maintains clarity and fairness in its fee mechanism, outlining specific details for protocol fees and brokerage commissions.
+- **Administrative Oversight:** Tokei goes beyond basic functions, offering advanced features for administrative control, such as fee settings, revenue claims, and NFT descriptor management.
+
+## A Closer Look at Tokeiโs Mechanics on Starknet
+
+Tokeiโs infrastructure skillfully manages the lifecycle of token streams, from creation to conclusion, ensuring a seamless experience.
+
+### Stream Creation
+
+Users can initiate streams, defining specific durations, total amounts, types of assets, and other custom conditions.
+
+### Stream Management and Oversight
+
+The protocol offers a comprehensive suite of functions for detailed stream analysis, including asset specifics, key milestones, deposited sums, and current status.
+
+### Stream Conclusion
+
+Streams can be concluded as per predefined conditions, with an efficient reallocation of funds based on the amount streamed and the remaining balance.
+
+### Withdrawals and Ownership Transfers
+
+Tokei allows for flexible withdrawals from streams and ownership transfers, with NFTs playing a pivotal role in facilitating these transactions.
+
+## Practical Applications and Implications
+
+Tokei opens up a world of possibilities within DeFi:
+
+- **Employee Vesting:** Enterprises can use Tokei for systematic token distribution to employees, ensuring transparency and fairness in vesting processes.
+- **Subscription Services:** The protocol can serve as a novel payment method for subscription services, streamlining transactions.
+- **DAO Operations:** DAOs can leverage Tokei for managing ongoing contributions and distributions, thereby bolstering community engagement and financial stewardship.
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index b93d631..d8c99bb 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -6,3 +6,5 @@
- [Prerequisites](getting-started/prerequisites.md)
- [Build the contracts](getting-started/build.md)
- [Test the contracts](getting-started/test.md)
+ - [Interact with contract as a User](getting-started/user.md)
+ - [Interact with contract as an Admin](getting-started/admin.md)
diff --git a/book/src/getting-started/README.md b/book/src/getting-started/README.md
index a58b70f..e55d4b0 100644
--- a/book/src/getting-started/README.md
+++ b/book/src/getting-started/README.md
@@ -1,3 +1,151 @@
-# Getting Started
+# Getting Started with Tokei Lockup Linear Contract
+Welcome to the Tokei Lockup Linear Contract on StarkNet. This guide will provide you with all the necessary information to get started with building, testing, and interacting with the contract, as well as administering it effectively.
-In this section, we will go through the steps to build and test the smart contracts.
+## ๐Table of Contents
+- [About Tokei](#about-tokei)
+- [Prerequisites](#prerequisites)
+- [Building the Contracts](#building-the-contracts)
+- [Testing the Contracts](#testing-the-contracts)
+- [User Interaction CLI Tool](#user-interaction-cli-tool)
+- [Admin Interaction Script](#admin-interaction-script)
+
+## About Tokei
+In the digital age where blockchain technology is reshaping financial landscapes, the Tokei protocol emerges as a cutting-edge solution for token streaming. Inspired by Sablier's groundbreaking work, Tokei harnesses the power of Starknet's Layer 2 capabilities to offer a novel and secure approach to token allocation and distribution, setting a new paradigm in decentralized finance (DeFi).
+
+## **The Advent of Tokei: A New Era in Token Streaming**
+
+At its essence, token streaming facilitates the continuous transfer of assets over time. Tokei elevates this concept by providing enhanced flexibility and customizability on Starknet, enabling users to create tailored streams that fit a plethora of financial scenarios.
+
+### **Distinctive Features of Tokei**
+
+- **Linear Lockup Streams**: Tokei's lockup linear streams are designed for a gradual and controlled release of funds, ideal for structured payment plans and corporate vesting schedules.
+- **Customization at Its Core**: With adjustable parameters like start and end times, cliff durations, and options for cancelability and transferability, Tokei caters to diverse streaming needs.
+- **Integration with ERC Standards**: Tokei supports ERC-20 tokens for streaming transactions and leverages ERC-721 (NFTs) to represent stream ownership, expanding its utility across asset classes.
+- **Transparent Fee Structure**: The platform ensures fair dealings with a clear-cut mechanism for protocol fees and brokerage commissions.
+- **Administrative Oversight**: Advanced features allow for administrative interventions, including setting fees, claiming revenues, and managing NFT descriptors.
+
+## **A Closer Look at Tokei's Mechanics on Starknet**
+
+Tokei's smart contract infrastructure orchestrates the entire lifecycle of token streams, from their inception to conclusion.
+
+### **Stream Creation**
+
+Participants can initiate streams with specific durations or date ranges, defining the total amount and the type of asset, as well as other bespoke conditions.
+
+### **Stream Management and Oversight**
+
+The protocol provides a suite of functions for obtaining comprehensive details about each stream, such as asset details, key timepoints, deposited sums, and the stream's current state.
+
+### **Stream Conclusion**
+
+Streams can be terminated based on predefined conditions, with the protocol adeptly reallocating funds relative to the amount streamed and the remaining balance.
+
+### **Withdrawals and Ownership Transfers**
+
+Users can withdraw from streams or transfer ownership, facilitated by NFT representations, further emphasizing Tokei's commitment to flexibility and security.
+
+## **Practical Applications and Implications**
+
+Tokei's protocol unlocks a myriad of possibilities within the DeFi ecosystem:
+
+- **Employee Vesting**: For example, enterprises can implement Tokei to distribute tokens to employees systematically, thereby ensuring a transparent vesting journey.
+- **Subscription Services**: Businesses can adopt token streaming as a payment method for subscription services, providing a seamless transaction flow.
+- **DAO Operations**: DAOs can employ Tokei to regulate ongoing contributions and distributions, enhancing community engagement and financial management.
+
+## **Visualizing Token Streaming with Tokei**
+
+Let's visualize the process with an example:
+
+- **Scenario**: Evelyn wishes to stream 10,000 XYZ tokens to Jordan over three months.
+- **Execution**: She locks the XYZ tokens into Tokei's smart contract at the start of April, with the stream set to end in July.
+- **Token Accrual**: As the days of April unfold, Jordan's balance begins to grow in real-time.
+- **Withdrawal Option**: By the 10th of April, Jordan has access to a portion of the tokens and can choose to withdraw at any time.
+
+*The graph here depicts a consistent token stream without a cliff.*
+
+
+
+
+Should Evelyn choose to retract her tokens, she has the autonomy to cancel the stream and retrieve the unstreamed tokens.
+
+### **Introducing Cliffs for Enhanced Control**
+
+Tokei introduces the concept of cliffs to add a strategic pause in the token release schedule:
+
+- **Vesting with Cliffs**: Consider a business that sets a 6-month cliff for an employee's token vesting, followed by a 2-year linear stream. If the employee exits the company prematurely, the business can cancel the stream and secure the assets not yet streamed.
+
+*This graph showcases a Lockup Linear stream with a cliff. The horizontal plateau represents the cliff duration, after which the linear increase in streamed tokens resumes.*
+
+
+
+
+## ๐Prerequisites
+Before you begin, make sure you have the following prerequisites installed:
+
+- Starknet Foundry
+- Scarb
+- Starknet.js
+- Node.js (for User and Admin scripts)
+- TypeScript
+
+
+
+More details: [Starknet Foundry](https://foundry-rs.github.io/starknet-foundry/)
+
+## ๐๏ธ Building the Contracts
+To build the smart contracts, open a terminal and execute:
+
+```bash
+scarb build
+```
+This command will compile the smart contracts and output them to the target directory. For a detailed structure of the output, see [build.md]((https://github.com/Akashneelesh/tokei/blob/main/book/src/getting-started/build.md)).
+
+## ๐จTesting the Contracts
+Run the following command to test the contracts:
+```bash
+snforge test
+```
+This executes tests located in the tests directory. Sample output:
+
+```
+Collected 38 test(s) from tokei package
+Running 38 test(s) from src/
+[PASS] tokei::tests::... (detailed test results)
+```
+
+For more detailed view about the test cases see [test.md](https://github.com/Akashneelesh/tokei/blob/main/book/src/getting-started/test.md).
+
+## โ User Interaction CLI Tool
+The User Interaction CLI tool is designed for easy interaction with the streaming contract. It supports various functionalities such as creating streams, withdrawing funds, and querying stream details. To use this tool, you need to install certain dependencies and follow the steps outlined in [user.md](https://github.com/Akashneelesh/tokei/blob/main/book/src/getting-started/user.md).
+
+### Key functionalities include:
+
+- Stream creation and management
+- Asset management
+- Fee and NFT integration
+
+For detailed instructions on installation, usage, and examples, please refer to [user.md](https://github.com/Akashneelesh/tokei/blob/main/book/src/getting-started/user.md).
+
+## ๐ฎโโ๏ธ Admin Interaction Script
+The Admin Interaction Script facilitates the management of the Tokei Lockup Linear Contract. It includes features for setting and retrieving protocol fees, claiming protocol revenues, and viewing administrative details.
+
+### Key aspects include:
+- Stream creation and management
+- Fee handling
+- NFT Integration
+- Core functionalities and key structures
+For complete guidelines on installation, usage, and available functions, see [admin.md](https://github.com/Akashneelesh/tokei/blob/main/book/src/getting-started/admin.md).
+
+## ๐ Resources
+
+Here are some resources to help you get started:
+
+- [Cairo Book](https://book.cairo-lang.org/)
+- [Starknet Book](https://book.starknet.io/)
+- [Starknet Foundry Book](https://foundry-rs.github.io/starknet-foundry/)
+- [Starknet By Example](https://starknet-by-example.voyager.online/)
+- [Starkli Book](https://book.starkli.rs/)
+
+## ๐ License
+
+This project is licensed under the **MIT license**. See [LICENSE](LICENSE) for more information.
diff --git a/book/src/getting-started/admin.md b/book/src/getting-started/admin.md
new file mode 100644
index 0000000..0945f80
--- /dev/null
+++ b/book/src/getting-started/admin.md
@@ -0,0 +1,128 @@
+# StarkNet Admin Interaction Script
+
+## ๐ Overview
+
+The Tokei Lockup Linear Contract on StarkNet facilitates the creation and management of time-bound asset streams. This guide delves into the functionalities, fee structures, and key contract components like `Durations`, `Range`, `Broker`, and `LockupLinearStream`.
+The Tokei Lockup Linear Contract offers a comprehensive solution for asset distribution on StarkNet. Its sophisticated structures enable precise control over asset streaming, making it an essential tool for DeFi applications. A deep understanding of its fee calculations and time-bound mechanisms is key for effective implementation.
+
+## ๐ช Core Functionalities
+
+### Stream Creation and Management
+
+- Create asset streams with specific timings using the `Range` structure.
+- Support for cancelable and transferable streams.
+- Functions for withdrawing assets according to the stream schedule.
+
+### Fee Handling
+
+- Management of protocol and broker fees based on the total stream amount.
+- Limitations to prevent excessive fee charges.
+
+### NFT Integration
+
+- Representation of each asset stream as an NFT for ease of tracking and transferability.
+
+## ๐ Key Structures and Functions
+
+### Fee Calculation (`check_and_calculate_fees`)
+
+- **Inputs**: Total stream amount, protocol fee percentage, broker fee percentage, max fee limit.
+- **Outputs**: Calculated deposit, protocol fee, and broker fee in `CreateAmounts` struct.
+- **Process**:
+ - Validation of protocol and broker fees against the maximum fee limit.
+ - Calculation of absolute fee amounts.
+ - Verification that total amount covers both fees, with remainder as the deposit amount.
+
+### PercentageMath Trait
+
+- Utilized for calculating fee percentages as a proportion of the total amount.
+
+### Scaled Division (`scaled_down_div`)
+
+- Function for dividing with scaling, used in time-related calculations.
+
+### Range and Durations
+
+- **Range**: Defines timestamps for the start, cliff, and end of an asset stream.
+- **Durations**: Represents cliff and total duration in time units.
+
+### LockupLinearStream Structure
+
+- Represents an asset stream, including financial tracking through `LockupAmounts`.
+- **LockupAmounts**: Encapsulates deposited, withdrawn, and refunded amounts.
+
+### Validation Checks (`check_create_with_range`)
+
+- Ensures timing integrity and deposit amount of the stream.
+- Checks for valid timing sequence and that current time precedes the end time.
+
+## Features
+- Setting and retrieving protocol fees
+- Claiming protocol revenues
+- Viewing administrative details
+- Dynamic interaction with smart contracts
+
+## ๐๏ธ Prerequisites
+Before running this script, ensure you have the following installed:
+- Node.js and npm
+- StarkNet libraries and ethers.js
+- TypeScript and ts-node
+
+## โ๏ธ Installation
+To set up the script on your local machine, follow these steps:
+1. Clone the GitHub repository:
+2. Navigate to the repository directory and install the required dependencies:
+
+```bash
+npm install or yarn
+```
+
+## ๐ ๏ธ Build the contracts
+```bash
+scarb build
+```
+
+## ๐คบ Usage
+To execute the script, run the following command in the root directory of the project:
+```bash
+npx ts-node src/scripts/admin_interaction.ts
+```
+
+Upon running the command, the script will initiate a CLI, prompting you to select from a range of functions for execution. Follow the on-screen instructions to interact with the smart contracts.
+
+## Available Functions
+The script offers various functionalities, categorized into "view" and "write" operations:
+
+### View Functions
+- `get_admin`
+- `get_flash_fee`
+- `get_protocol_fee`
+- `get_protocol_revenues`
+
+### Write Functions
+- `set_flash_fee`
+- `set_protocol_fee`
+- `claim_protocol_revenues`
+
+## ๐ฐSetting Protocol Fees
+
+As an administrator, you can adjust the protocol fees charged by Tokei. Here's how to set the protocol fee using the **`set_protocol_fee`**
+- Enter the function name in this case : set_protocol_fee
+- You will be prompted with the protocol fee to be set, enter the amount.
+- And that's all you would have successfully set the protocol fee.
+
+Here's an example of how you can set the protocol fee :
+
+
+## Getting the Protocol Fees
+Here's an example of how you can retrieve the protocol fee
+function:
+For each function, you will be prompted to input the necessary parameters before execution.
+
+## **Finalizing Your Admin Tasks**
+
+After you've set fees or claimed revenues, verify the transactions have processed successfully by checking the Starknet block explorer with the transaction hashes output by the script.
+
+## **Conclusion**
+
+Administering the Tokei protocol requires a careful approach to manage fees and revenues effectively. By following the steps outlined in this guide, you can confidently execute administrative tasks, ensuring Tokei continues to operate smoothly on the Starknet platform.
diff --git a/book/src/getting-started/build.md b/book/src/getting-started/build.md
index 4b79cce..d1fcfb4 100644
--- a/book/src/getting-started/build.md
+++ b/book/src/getting-started/build.md
@@ -13,10 +13,8 @@ Sample output:
```shell
tree target/dev
โโโ tokei.starknet_artifacts.json
-โโโ tokei_ERC20.casm.json
-โโโ tokei_ERC20.sierra.json
-โโโ tokei_ERC721.casm.json
-โโโ tokei_ERC721.sierra.json
-โโโ tokei_TokeiLockupLinear.casm.json
-โโโ tokei_TokeiLockupLinear.sierra.json
+โโโ tokei_ERC20.compiled_contract_class.json
+โโโ tokei_ERC20.contract_class.json
+โโโ tokei_TokeiLockupLinear.compiled_contract_class.json
+โโโ tokei_TokeiLockupLinear.contract_class.json
```
diff --git a/book/src/getting-started/prerequisites.md b/book/src/getting-started/prerequisites.md
index 8613d42..589da6d 100644
--- a/book/src/getting-started/prerequisites.md
+++ b/book/src/getting-started/prerequisites.md
@@ -1,3 +1,6 @@
# Prerequisites
- [Starknet Foundry](https://foundry-rs.github.io/starknet-foundry/)
+- [Scarb](https://docs.swmansion.com/scarb/download)
+- [Starknet.js](https://www.starknetjs.com/docs/guides/intro/)
+- [Node.js](https://nodejs.org/en)
diff --git a/book/src/getting-started/test.md b/book/src/getting-started/test.md
index 4f0f67d..5722f7f 100644
--- a/book/src/getting-started/test.md
+++ b/book/src/getting-started/test.md
@@ -11,9 +11,45 @@ This will execute the tests in `tests` directory and print the results.
Sample output:
```shell
-Collected 1 test(s) and 2 test file(s)
-Running 0 test(s) from src/lib.cairo
-Running 1 test(s) from tests/test_lockup_linear.cairo
-[PASS] test_lockup_linear::test_lockup_linear::given_normal_conditions_when_create_with_range_then_expected_results
-Tests: 1 passed, 0 failed, 0 skipped
+Collected 38 test(s) from tokei package
+Running 38 test(s) from src/
+[PASS] tokei::tests::test_lockup_linear::test_set_nft_descriptor, gas: ~13.94
+[PASS] tokei::tests::test_lockup_linear::test_set_protocol_fee, gas: ~25.6
+[PASS] tokei::tests::test_lockup_linear::test_create_with_duration_when_cliff_is_less_than_start, gas: ~51.1
+[PASS] tokei::tests::test_lockup_linear::given_normal_conditions_when_create_with_range_then_expected_results, gas: ~111.27
+[PASS] tokei::tests::test_lockup_linear::test_create_stream_with_range, gas: ~179.08
+[PASS] tokei::tests::test_lockup_linear::test_create_with_duration_when_amount_is_zero, gas: ~50.1
+[PASS] tokei::tests::test_lockup_linear::test_create_with_duration, gas: ~364.33
+[PASS] tokei::tests::test_lockup_linear::test_all_the_getters_with_respect_to_stream, gas: ~480.77
+[PASS] tokei::tests::test_lockup_linear::test_get_cliff_time, gas: ~207.51
+[PASS] tokei::tests::test_lockup_linear::test_get_cliff_time_when_null, gas: ~207.27
+[PASS] tokei::tests::test_lockup_linear::test_get_range, gas: ~208.89000000000001
+[PASS] tokei::tests::test_lockup_linear::test_get_range_when_null, gas: ~208.38
+[PASS] tokei::tests::test_lockup_linear::test_get_stream_when_status_settled, gas: ~351.85
+[PASS] tokei::tests::test_lockup_linear::test_get_stream_when_not_settled, gas: ~351.85
+[PASS] tokei::tests::test_lockup_linear::test_streamed_amount_of_cliff_time_in_past, gas: ~223.88
+[PASS] tokei::tests::test_lockup_linear::test_streamed_amount_of_cliff_time_in_present, gas: ~280.13
+[PASS] tokei::tests::test_lockup_linear::test_streamed_amount_of_cliff_time_in_present_1, gas: ~258.25
+[PASS] tokei::tests::test_lockup_linear::test_withdrawable_amount_of_cliff_time, gas: ~275.78000000000003
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_by_recipient, gas: ~364.54
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_by_recipient_before_total_time, gas: ~379.75
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_by_approved_caller, gas: ~376.92
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_by_approved_caller_to_other_address_than_recipient, gas: ~238.97
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_by_unapproved_caller, gas: ~277.65000000000003
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_by_caller, gas: ~362.18
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_max, gas: ~360.06
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_max_and_transfer, gas: ~406.89
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_max_and_transfer_when_not_transferable, gas: ~162.88
+[PASS] tokei::tests::test_lockup_linear::test_burn_token_when_depleted, gas: ~374.87
+[PASS] tokei::tests::test_lockup_linear::test_withdraw_multiple, gas: ~754.25
+[PASS] tokei::tests::test_lockup_linear::test_cancel_should_panic, gas: ~353.44
+[PASS] tokei::tests::test_lockup_linear::test_cancel, gas: ~434.05
+[PASS] tokei::tests::test_lockup_linear::test_burn_token_when_not_depleted, gas: ~381.96
+[PASS] tokei::tests::test_lockup_linear::test_renounce, gas: ~313.89
+[PASS] tokei::tests::test_lockup_linear::test_renounce_by_recipient, gas: ~200.66
+[PASS] tokei::tests::test_lockup_linear::test_transfer_admin, gas: ~179.75
+[PASS] tokei::tests::test_lockup_linear::test_set_protocol_fee_panic, gas: ~200.28
+[PASS] tokei::tests::test_lockup_linear::test_set_flash_fee_panic, gas: ~200.11
+[PASS] tokei::tests::test_lockup_linear::test_set_flash_fee, gas: ~182.27
+Tests: 38 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
```
diff --git a/book/src/getting-started/user.md b/book/src/getting-started/user.md
new file mode 100644
index 0000000..4a28965
--- /dev/null
+++ b/book/src/getting-started/user.md
@@ -0,0 +1,118 @@
+# User Interaction CLI Tool
+
+## ๐ Overview
+This command-line interface (CLI) tool is designed for interacting with a streaming contract. It provides an easy-to-use interface to execute various functions related to asset streaming, such as creating streams, withdrawing funds, and querying stream details.
+
+The Tokei Lockup Linear Contract is part of the Tokei protocol, designed to manage financial streams over time. This contract is particularly relevant for users interested in creating, managing, and interacting with asset streams. Key functionalities include:
+Detailed Functionalities:
+__________________________________________________________________________________________________________________________________________________________________
+### ๐ Stream Creation:
+
+Users can initiate asset streams with precise timing. The contract allows specification of start, cliff, and end times, controlling the asset flow.
+
+Durations Structure: This struct includes two key time periods - the cliff duration and the total duration. The cliff duration is a waiting period before the asset distribution starts, while the total duration is the entire span of the asset stream.
+
+Range Structure: Defines the specific timing of the stream with start, cliff, and end timestamps. This granularity offers users flexibility in how they schedule their asset distribution.
+__________________________________________________________________________________________________________________________________________________________________
+## ๐ฐAsset Management:
+### Cancelable Streams:
+Users have the option to cancel streams, providing flexibility and control over their asset distribution.
+__________________________________________________________________________________________________________________________________________________________________
+
+### ๐ Transferable Rights:
+Streams can be made transferable, allowing users to assign their streaming rights to others, adding an element of liquidity to their assets.
+__________________________________________________________________________________________________________________________________________________________________
+
+### ๐ง Withdrawals:
+Assets can be withdrawn as per the defined schedule, offering users timely access to their funds.
+__________________________________________________________________________________________________________________________________________________________________
+
+### ๐จ Fee and NFT Integration:
+The contract entails fees like protocol fees and broker fees, calculated as percentages of the total stream amount.
+Each stream is uniquely represented as an NFT, providing a tangible asset that can be held, transferred, or traded.
+__________________________________________________________________________________________________________________________________________________________________
+### Structures in Detail:
+
+#### Duration:
+This structure is key for defining the time frame of an asset stream. It includes two components:
+#### Cliff:
+The initial period during which no assets are distributed.
+#### Total:
+The entire length of the asset stream, from start to finish.
+#### Range:
+This structure provides a more detailed breakdown of the stream's timeline, with specific timestamps marking the start, cliff, and end of the asset distribution.
+__________________________________________________________________________________________________________________________________________________________________
+## ๐ Prerequisites
+Before running this tool, ensure you have the following prerequisites installed:
+- Node.js
+- TypeScript
+- ts-node
+
+## โ๏ธ Installation
+1. Clone the repository to your local machine.
+2. Navigate to the project directory.
+3. Install the dependencies using npm:
+
+```bash
+npm install or yarn
+```
+
+## ๐ฌ Usage
+To start the CLI tool, run the following command in the terminal:
+
+```bash
+npx ts-node src/scripts/user_interaction.ts
+```
+
+### ๐คบ Interactive CLI
+Once the script is running, you will be presented with an interactive CLI. The CLI offers a range of 'view' and 'external' functions that you can call and invoke.
+
+### Executing Functions
+Follow these steps to execute a function:
+1. Choose a function from the list provided by the CLI.
+2. Enter the function name when prompted.
+3. You will then be prompted to provide the necessary parameters for the chosen function.
+4. After entering the parameters, the function will be invoked, and the output (if any) will be displayed in the CLI.
+
+### Available Functions
+The tool provides various functions, categorized as 'view' and 'external' functions. Here's a brief overview:
+
+#### View Functions
+These functions are used to view details about streams. Examples include:
+- `get_asset`
+- `get_protocol_fee`
+- `get_protocol_revenues`
+- `get_cliff_time`
+- ...and more
+
+#### External Functions
+These functions allow you to perform actions like creating or canceling streams. Examples include:
+- `create_with_duration`
+- `create_with_range`
+- `cancel_stream`
+- `withdraw_max`
+- ...and more
+
+### ๐ Example Workflow of invoking a write function
+1. Choose a function, e.g., `create_with_duration`.
+2. Provide the required parameters like sender, recipient, amount, asset, etc.
+3. The function will be executed, and the transaction hash or relevant information will be displayed.
+An example of a transaction :
+
+
+### Example Workflow of invoking a read function
+1. Choose a function, e.g., `get_range`.
+2. Provide the stream ID
+3. The function will return back with the relevant information.
+An example of a transaction :
+
+
+### Example Inputs
+- The example inputs are available in src/scripts/user_example.txt.
+- You could refer to it for executing a test stream.
+
+## Error Handling
+If an error occurs during the execution of a function, the tool will display an error message. Please ensure that all parameters are entered correctly and in the required format.
+
+## Closing the CLI
+To exit the CLI, type `quit` at any prompt.
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..4ea691b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,552 @@
+{
+ "name": "tokei",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "tokei",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "dotenv": "^16.4.1",
+ "starknet": "^5.27.0"
+ }
+ },
+ "node_modules/@noble/curves": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz",
+ "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==",
+ "dependencies": {
+ "@noble/hashes": "1.3.3"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
+ "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@rometools/cli-darwin-arm64": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/@rometools/cli-darwin-arm64/-/cli-darwin-arm64-12.1.3.tgz",
+ "integrity": "sha512-AmFTUDYjBuEGQp/Wwps+2cqUr+qhR7gyXAUnkL5psCuNCz3807TrUq/ecOoct5MIavGJTH6R4aaSL6+f+VlBEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rometools/cli-darwin-x64": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/@rometools/cli-darwin-x64/-/cli-darwin-x64-12.1.3.tgz",
+ "integrity": "sha512-k8MbWna8q4LRlb005N2X+JS1UQ+s3ZLBBvwk4fP8TBxlAJXUz17jLLu/Fi+7DTTEmMhM84TWj4FDKW+rNar28g==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rometools/cli-linux-arm64": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/@rometools/cli-linux-arm64/-/cli-linux-arm64-12.1.3.tgz",
+ "integrity": "sha512-X/uLhJ2/FNA3nu5TiyeNPqiD3OZoFfNfRvw6a3ut0jEREPvEn72NI7WPijH/gxSz55znfQ7UQ6iM4DZumUknJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rometools/cli-linux-x64": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/@rometools/cli-linux-x64/-/cli-linux-x64-12.1.3.tgz",
+ "integrity": "sha512-csP17q1eWiUXx9z6Jr/JJPibkplyKIwiWPYNzvPCGE8pHlKhwZj3YHRuu7Dm/4EOqx0XFIuqqWZUYm9bkIC8xg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rometools/cli-win32-arm64": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/@rometools/cli-win32-arm64/-/cli-win32-arm64-12.1.3.tgz",
+ "integrity": "sha512-RymHWeod57EBOJY4P636CgUwYA6BQdkQjh56XKk4pLEHO6X1bFyMet2XL7KlHw5qOTalzuzf5jJqUs+vf3jdXQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rometools/cli-win32-x64": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/@rometools/cli-win32-x64/-/cli-win32-x64-12.1.3.tgz",
+ "integrity": "sha512-yHSKYidqJMV9nADqg78GYA+cZ0hS1twANAjiFibQdXj9aGzD+s/IzIFEIi/U/OBLvWYg/SCw0QVozi2vTlKFDQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@scure/base": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz",
+ "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==",
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/starknet": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@scure/starknet/-/starknet-1.0.0.tgz",
+ "integrity": "sha512-o5J57zY0f+2IL/mq8+AYJJ4Xpc1fOtDhr+mFQKbHnYFmm3WQrC+8zj2HEgxak1a+x86mhmBC1Kq305KUpVf0wg==",
+ "dependencies": {
+ "@noble/curves": "~1.3.0",
+ "@noble/hashes": "~1.3.3"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/abi-wan-kanabi": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/abi-wan-kanabi/-/abi-wan-kanabi-1.0.3.tgz",
+ "integrity": "sha512-Xwva0AnhXx/IVlzo3/kwkI7Oa7ZX7codtcSn+Gmoa2PmjGPF/0jeVud9puasIPtB7V50+uBdUj4Mh3iATqtBvg==",
+ "dependencies": {
+ "abi-wan-kanabi": "^1.0.1",
+ "fs-extra": "^10.0.0",
+ "rome": "^12.1.3",
+ "typescript": "^4.9.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "generate": "dist/generate.js"
+ }
+ },
+ "node_modules/abi-wan-kanabi-v1": {
+ "name": "abi-wan-kanabi",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/abi-wan-kanabi/-/abi-wan-kanabi-1.0.3.tgz",
+ "integrity": "sha512-Xwva0AnhXx/IVlzo3/kwkI7Oa7ZX7codtcSn+Gmoa2PmjGPF/0jeVud9puasIPtB7V50+uBdUj4Mh3iATqtBvg==",
+ "dependencies": {
+ "abi-wan-kanabi": "^1.0.1",
+ "fs-extra": "^10.0.0",
+ "rome": "^12.1.3",
+ "typescript": "^4.9.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "generate": "dist/generate.js"
+ }
+ },
+ "node_modules/abi-wan-kanabi-v2": {
+ "name": "abi-wan-kanabi",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/abi-wan-kanabi/-/abi-wan-kanabi-2.1.1.tgz",
+ "integrity": "sha512-rXjOT34DkZSLlje7ZsU5qQ04txbNftwJKPlKQxVjbKqMe43RomC9VVTips3+gb9k9gQc3+TQ8UGjY2yDhXF1Vw==",
+ "dependencies": {
+ "ansicolors": "^0.3.2",
+ "cardinal": "^2.1.1",
+ "fs-extra": "^10.0.0",
+ "rome": "^12.1.3",
+ "typescript": "^5.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "generate": "dist/generate.js"
+ }
+ },
+ "node_modules/abi-wan-kanabi-v2/node_modules/typescript": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/ansicolors": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
+ "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="
+ },
+ "node_modules/cardinal": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz",
+ "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==",
+ "dependencies": {
+ "ansicolors": "~0.3.2",
+ "redeyed": "~2.1.0"
+ },
+ "bin": {
+ "cdl": "bin/cdl.js"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/dotenv": {
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
+ "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/motdotla/dotenv?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isomorphic-fetch": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
+ "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
+ "dependencies": {
+ "node-fetch": "^2.6.1",
+ "whatwg-fetch": "^3.4.1"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/lossless-json": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-2.0.11.tgz",
+ "integrity": "sha512-BP0vn+NGYvzDielvBZaFain/wgeJ1hTvURCqtKvhr1SCPePdaaTanmmcplrHfEJSJOUql7hk4FHwToNJjWRY3g=="
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pako": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
+ },
+ "node_modules/redeyed": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz",
+ "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==",
+ "dependencies": {
+ "esprima": "~4.0.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rome": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/rome/-/rome-12.1.3.tgz",
+ "integrity": "sha512-e+ff72hxDpe/t5/Us7YRBVw3PBET7SeczTQNn6tvrWdrCaAw3qOukQQ+tDCkyFtS4yGsnhjrJbm43ctNbz27Yg==",
+ "hasInstallScript": true,
+ "bin": {
+ "rome": "bin/rome"
+ },
+ "engines": {
+ "node": ">=14.*"
+ },
+ "optionalDependencies": {
+ "@rometools/cli-darwin-arm64": "12.1.3",
+ "@rometools/cli-darwin-x64": "12.1.3",
+ "@rometools/cli-linux-arm64": "12.1.3",
+ "@rometools/cli-linux-x64": "12.1.3",
+ "@rometools/cli-win32-arm64": "12.1.3",
+ "@rometools/cli-win32-x64": "12.1.3"
+ }
+ },
+ "node_modules/starknet": {
+ "version": "5.27.0",
+ "resolved": "https://registry.npmjs.org/starknet/-/starknet-5.27.0.tgz",
+ "integrity": "sha512-pEMzKeaHTej8W0SZjZXCi81fHNIY15wlewYxf8Y6GT1YLClN+TlBeJ0mq+vD4UerTKTAgK794kCfCCeuAizfbQ==",
+ "dependencies": {
+ "@noble/curves": "~1.3.0",
+ "@scure/base": "~1.1.3",
+ "@scure/starknet": "~1.0.0",
+ "abi-wan-kanabi-v1": "npm:abi-wan-kanabi@^1.0.3",
+ "abi-wan-kanabi-v2": "npm:abi-wan-kanabi@^2.1.1",
+ "isomorphic-fetch": "^3.0.0",
+ "lossless-json": "^2.0.8",
+ "pako": "^2.0.4",
+ "url-join": "^4.0.1"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/whatwg-fetch": {
+ "version": "3.6.20",
+ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
+ "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..32def87
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "tokei",
+ "version": "1.0.0",
+ "description": "
",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "dotenv": "^16.4.1",
+ "starknet": "^5.27.0"
+ }
+}
diff --git a/src/core/interface.cairo b/src/core/interface.cairo
new file mode 100644
index 0000000..40cebf0
--- /dev/null
+++ b/src/core/interface.cairo
@@ -0,0 +1,153 @@
+use starknet::ContractAddress;
+
+
+#[starknet::interface]
+trait ITokeiLockupLinearERC721Snake {
+ /// Returns the number of NFTs owned by `account`.
+ fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
+
+ /// Returns the owner address of `token_id`.
+ ///
+ /// Arguements:
+ ///
+ /// - `token_id` exists.
+ fn owner_of(self: @TContractState, token_id: u128) -> ContractAddress;
+
+ /// Returns the address approved for `token_id`.
+ ///
+ /// Arguements:
+ ///
+ /// - `token_id` exists.
+ fn get_approved(self: @TContractState, token_id: u128) -> ContractAddress;
+
+ /// Query if `operator` is an authorized operator for `owner`.
+ fn is_approved_for_all(
+ self: @TContractState, owner: ContractAddress, operator: ContractAddress
+ ) -> bool;
+
+ /// Change or reaffirm the approved address for an NFT.
+ ///
+ /// Arguements:
+ ///
+ /// - The caller is either an approved operator or the `token_id` owner.
+ /// - `to` cannot be the token owner.
+ /// - `token_id` exists.
+ ///
+ /// Emits an `Approval` event.
+ fn approve(ref self: TContractState, to: ContractAddress, token_id: u128);
+
+ /// Enable or disable approval for `operator` to manage all of the
+ /// caller's assets.
+ ///
+ /// Arguements:
+ ///
+ /// - `operator` cannot be the caller.
+ ///
+ /// Emits an `Approval` event.
+ fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool);
+
+ /// Transfers ownership of `token_id` from `from` to `to`.
+ ///
+ /// Arguements:
+ ///
+ /// - Caller is either approved or the `token_id` owner.
+ /// - `to` is not the zero address.
+ /// - `from` is not the zero address.
+ /// - `token_id` exists.
+ ///
+ /// Emits a `Transfer` event.
+ fn transfer_from(
+ ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u128
+ );
+
+ /// Transfers ownership of `token_id` from `from` if `to` is either an account or `IERC721Receiver`.
+ ///
+ /// `data` is additional data, it has no specified format and it is sent in call to `to`.
+ ///
+ /// Requirements:
+ ///
+ /// - Caller is either approved or the `token_id` owner.
+ /// - `to` is not the zero address.
+ /// - `from` is not the zero address.
+ /// - `token_id` exists.
+ /// - `to` is either an account contract or supports the `IERC721Receiver` interface.
+ ///
+ /// Emits a `Transfer` event.
+ fn safe_transfer_from(
+ ref self: TContractState,
+ from: ContractAddress,
+ to: ContractAddress,
+ token_id: u128,
+ data: Span
+ );
+}
+
+#[starknet::interface]
+trait ITokeiLockupLinearERC721Camel {
+ /// Returns the number of NFTs owned by `account`.
+ fn balanceOf(self: @TContractState, account: ContractAddress) -> u256;
+
+ /// Returns the owner address of `token_id`.
+ ///
+ /// Arguements:
+ ///
+ /// - `token_id` exists.
+ fn ownerOf(self: @TContractState, token_id: u128) -> ContractAddress;
+
+ /// Returns the address approved for `token_id`.
+ ///
+ /// Arguements:
+ ///
+ /// - `token_id` exists.
+ fn getApproved(self: @TContractState, token_id: u128) -> ContractAddress;
+
+ /// Query if `operator` is an authorized operator for `owner`.
+ fn isApprovedForAll(
+ self: @TContractState, owner: ContractAddress, operator: ContractAddress
+ ) -> bool;
+
+ /// Enable or disable approval for `operator` to manage all of the
+ /// caller's assets.
+ ///
+ /// Arguements:
+ ///
+ /// - `operator` cannot be the caller.
+ ///
+ /// Emits an `Approval` event.
+ fn setApprovalForAll(ref self: TContractState, operator: ContractAddress, approved: bool);
+
+ /// Transfers ownership of `token_id` from `from` to `to`.
+ ///
+ /// Arguements:
+ ///
+ /// - Caller is either approved or the `token_id` owner.
+ /// - `to` is not the zero address.
+ /// - `from` is not the zero address.
+ /// - `token_id` exists.
+ ///
+ /// Emits a `Transfer` event.
+ fn transferFrom(
+ ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u128
+ );
+
+ /// Transfers ownership of `token_id` from `from` if `to` is either an account or `IERC721Receiver`.
+ ///
+ /// `data` is additional data, it has no specified format and it is sent in call to `to`.
+ ///
+ /// Requirements:
+ ///
+ /// - Caller is either approved or the `token_id` owner.
+ /// - `to` is not the zero address.
+ /// - `from` is not the zero address.
+ /// - `token_id` exists.
+ /// - `to` is either an account contract or supports the `IERC721Receiver` interface.
+ ///
+ /// Emits a `Transfer` event.
+ fn safeTransferFrom(
+ ref self: TContractState,
+ from: ContractAddress,
+ to: ContractAddress,
+ token_id: u128,
+ data: Span
+ );
+}
diff --git a/src/core/lockup_linear.cairo b/src/core/lockup_linear.cairo
index 2b50c06..6131f50 100644
--- a/src/core/lockup_linear.cairo
+++ b/src/core/lockup_linear.cairo
@@ -9,7 +9,8 @@ use core::traits::Into;
use starknet::{ContractAddress, ClassHash};
// Local imports.
-use tokei::types::lockup_linear::{Range, Broker};
+use tokei::types::lockup_linear::{Range, Broker, LockupLinearStream, Durations};
+use tokei::types::lockup::{Status};
// ______ ____ __ __ ______ ____
// /_ __// __ \ / //_/ / ____// _/
@@ -34,6 +35,203 @@ use tokei::types::lockup_linear::{Range, Broker};
// *************************************************************************
#[starknet::interface]
trait ITokeiLockupLinear {
+ //////////////////////////////////////////////////////////////////////////
+ //USER-FACING CONSTANT FUNCTIONS
+ //////////////////////////////////////////////////////////////////////////
+
+ /// Returns the asset address of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_asset(self: @TContractState, stream_id: u64) -> ContractAddress;
+
+ /// Returns the next stream id.
+ /// # Arguments
+ fn get_next_stream_id(self: @TContractState) -> u64;
+
+ /// Returns the cliff time of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_cliff_time(self: @TContractState, stream_id: u64) -> u64;
+
+ /// Returns the deposited amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_deposited_amount(self: @TContractState, stream_id: u64) -> u256;
+
+ /// Returns the end time of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_end_time(self: @TContractState, stream_id: u64) -> u64;
+
+ /// Returns the Range of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_range(self: @TContractState, stream_id: u64) -> Range;
+
+ /// Returns the refundable amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_refunded_amount(self: @TContractState, stream_id: u64) -> u256;
+
+ /// Returns the sender address of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_sender(self: @TContractState, stream_id: u64) -> ContractAddress;
+
+ /// Returns the start time of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_start_time(self: @TContractState, stream_id: u64) -> u64;
+
+ /// Returns the stream state of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_stream(self: @TContractState, stream_id: u64) -> LockupLinearStream;
+
+ /// Returns the withdrawn amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_withdrawn_amount(self: @TContractState, stream_id: u64) -> u256;
+
+ /// Returns if the stream is cancelable.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_cancelable(self: @TContractState, stream_id: u64) -> bool;
+
+ /// Returns if the stream is transferable.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_transferable(self: @TContractState, stream_id: u64) -> bool;
+
+ /// Returns if the stream is depleted.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_depleted(self: @TContractState, stream_id: u64) -> bool;
+
+ /// Returns if the stream is canceled.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_stream(self: @TContractState, stream_id: u64) -> bool;
+
+ /// Returns the refundable amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn refundable_amount_of(self: @TContractState, stream_id: u64) -> u256;
+
+ /// Returns the recipient address of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_recipient(self: @TContractState, stream_id: u64) -> ContractAddress;
+
+ /// Returns a bool if the stream was canceled, settled, or depleted.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_cold(self: @TContractState, stream_id: u64) -> bool;
+
+ /// Returns a bool if the stream is streaming or pending.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_warm(self: @TContractState, stream_id: u64) -> bool;
+
+ /// Returns the withdrawable amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn withdrawable_amount_of(self: @TContractState, stream_id: u64) -> u256;
+
+ /// Returns the status of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn status_of(self: @TContractState, stream_id: u64) -> Status;
+
+ /// Returns the amount of tokens streamed.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn streamed_amount_of(self: @TContractState, stream_id: u64) -> u256;
+
+ /// Returns if the stream was canceled.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn was_canceled(self: @TContractState, stream_id: u64) -> bool;
+
+ /// Returns the amount of protocol revenues for the given asset.
+ /// # Arguments
+ /// * `asset` - The asset to claim the protocol revenues for.
+ fn get_protocol_revenues(self: @TContractState, asset: ContractAddress) -> u256;
+
+ /// Returns the protocol fee for the given asset.
+ /// # Arguments
+ /// * `asset` - The asset that has a set protocol fee.
+ fn get_protocol_fee(self: @TContractState, asset: ContractAddress) -> u256;
+
+ /// Returns the NFT descriptor address.
+ fn get_nft_descriptor(self: @TContractState) -> ContractAddress;
+
+ /// Returns the flash fee
+ fn get_flash_fee(self: @TContractState) -> u256;
+
+ /// Returns the admin address.
+ fn get_admin(self: @TContractState) -> ContractAddress;
+
+ /// Returns the streams of the sender.
+ /// # Arguments
+ /// * `sender` - The address of the sender.
+ /// # Returns
+ /// * `streams` - The streams of the sender.
+ fn get_streams_by_sender(
+ self: @TContractState, sender: ContractAddress
+ ) -> Span;
+
+ /// Returns the streams of the recipient.
+ /// # Arguments
+ /// * `recipient` - The address of the recipient.
+ /// # Returns
+ /// * `streams` - The streams of the recipient.
+ fn get_streams_by_recipient(
+ self: @TContractState, recipient: ContractAddress
+ ) -> Span;
+
+ /// Returns the streams ids of the sender.
+ /// # Arguments
+ /// * `sender` - The address of the sender.
+ /// # Returns
+ /// * `stream_ids` - The stream ids of the sender.
+ fn get_streams_ids_by_sender(self: @TContractState, sender: ContractAddress) -> Span;
+
+ /// Returns the streams ids of the recipient.
+ /// # Arguments
+ /// * `recipient` - The address of the recipient.
+ /// # Returns
+ /// * `stream_ids` - The stream ids of the recipient.
+ fn get_streams_ids_by_recipient(self: @TContractState, recipient: ContractAddress) -> Span;
+
+
+ //////////////////////////////////////////////////////////////////////////
+ //USER-FACING NON-CONSTANT FUNCTIONS
+ //////////////////////////////////////////////////////////////////////////
+
+ /// Create a new stream with a duration.
+ /// # Arguments
+ /// * `sender` - The address streaming the assets, with the ability to cancel the stream.
+ /// * `recipient` - The address receiving the assets.
+ /// * `total_amount` - The total amount of ERC-20 assets to be paid, including the stream deposit and any potential
+ /// * `asset` - The contract address of the ERC-20 asset used for streaming.
+ /// * `cancelable` - Indicates if the stream is cancelable.
+ /// * `transferable` - Indicates if the stream is transferable.
+ /// * `duration` - The duration of the stream. Struct containing (i) the stream's cliff period, (ii) total duration
+ /// * `broker` - The broker of the stream. Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the
+ /// percentage fee paid to the broker from `totalAmount`.
+ fn create_with_duration(
+ ref self: TContractState,
+ sender: ContractAddress,
+ recipient: ContractAddress,
+ total_amount: u256,
+ asset: ContractAddress,
+ cancelable: bool,
+ transferable: bool,
+ duration: Durations,
+ broker: Broker
+ ) -> u64;
+
/// Create a new stream.
/// # Arguments
/// * `sender` - The address streaming the assets, with the ability to cancel the stream.
@@ -50,12 +248,94 @@ trait ITokeiLockupLinear {
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
- total_amount: u128,
+ total_amount: u256,
asset: ContractAddress,
cancelable: bool,
+ transferable: bool,
range: Range,
broker: Broker,
) -> u64;
+
+ /// Burns the NFT token of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn burn_token(ref self: TContractState, stream_id: u64);
+
+ // /// Cancels the stream.
+ // /// # Arguments
+ // /// * `stream_id` - The id of the stream.
+ fn cancel(ref self: TContractState, stream_id: u64);
+
+ // /// Cancels multiple streams.
+ // /// # Arguments
+ // /// * `stream_ids` - The ids of the streams.
+ fn cancel_multiple(ref self: TContractState, stream_ids: Span);
+ /// Renounces the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn renounce(ref self: TContractState, stream_id: u64);
+
+ /// Sets the NFT descriptor.
+ /// # Arguments
+ /// * `nft_descriptor` - The NFT descriptor.
+ fn set_nft_descriptor(ref self: TContractState, nft_descriptor: ContractAddress);
+
+ /// Withdraws the stream's assets.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// * `to` - The address to withdraw the assets to.
+ /// * `amount` - The amount of assets to withdraw.
+ fn withdraw(ref self: TContractState, stream_id: u64, to: ContractAddress, amount: u256);
+
+ /// Withdraws the maximum amount of the stream's assets.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// * `to` - The address to withdraw the assets to.
+ fn withdraw_max(ref self: TContractState, stream_id: u64, to: ContractAddress);
+
+ /// Withdraws the stream's assets and transfers the NFT token.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// * `new_recipient` - The address to withdraw the assets to.
+ fn withdraw_max_and_transfer(
+ ref self: TContractState, stream_id: u64, new_recipient: ContractAddress
+ );
+
+ /// Withdraws multiple streams' assets.
+ /// # Arguments
+ /// * `stream_ids` - The ids of the streams.
+ /// * `to` - The address to withdraw the assets to.
+ /// * `amounts` - The amounts of assets to withdraw.
+ fn withdraw_multiple(
+ ref self: TContractState, stream_ids: Span, to: ContractAddress, amounts: Span
+ );
+
+ //Comptroller functions
+ /// Sets the flash fee.
+ /// # Arguments
+ /// * `new_flash_fee` - The new flash fee.
+ fn set_flash_fee(ref self: TContractState, new_flash_fee: u256);
+
+ /// Sets the protocol fee.
+ /// # Arguments
+ /// * `asset` - The asset to set the protocol fee for.
+ /// * `new_protocol_fee` - The new protocol fee.
+ fn set_protocol_fee(ref self: TContractState, asset: ContractAddress, new_protocol_fee: u256);
+
+ /// Toggle flash assets.
+ /// # Arguments
+ /// * `asset` - The asset to toggle.
+ fn toggle_flash_assets(ref self: TContractState, asset: ContractAddress);
+
+ /// Transfers the admin.
+ /// # Arguments
+ /// * `new_admin` - The new admin.
+ fn transfer_admin(ref self: TContractState, new_admin: ContractAddress);
+
+ /// Claims the protocol revenues.
+ /// # Arguments
+ /// * `asset` - The asset to claim the protocol revenues for.
+ fn claim_protocol_revenues(ref self: TContractState, asset: ContractAddress);
}
#[starknet::contract]
@@ -65,20 +345,52 @@ mod TokeiLockupLinear {
// *************************************************************************
// Core lib imports.
+ use tokei::core::lockup_linear::ITokeiLockupLinear;
+ use core::starknet::event::EventEmitter;
use core::result::ResultTrait;
use starknet::{
- get_caller_address, ContractAddress, contract_address_const, get_contract_address
+ get_caller_address, ContractAddress, contract_address_const, get_contract_address,
+ get_block_timestamp
};
use array::ArrayTrait;
use traits::Into;
use debug::PrintTrait;
+ use zeroable::Zeroable;
// Local imports.
- use tokei::types::lockup_linear::{Range, Broker, LockupLinearStream};
- use tokei::types::lockup::LockupAmounts;
- use tokei::tokens::erc721::{ERC721, IERC721};
- use tokei::tokens::erc20::{ERC20, IERC20, IERC20Dispatcher, IERC20DispatcherTrait};
+ use tokei::types::lockup_linear::{Range, Broker, LockupLinearStream, Durations};
+ use tokei::types::lockup::{Status, LockupAmounts};
+ use tokei::core::interface::{ITokeiLockupLinearERC721Snake, ITokeiLockupLinearERC721Camel};
+ use tokei::libraries::helpers::{
+ scaled_down_div, check_and_calculate_fees, check_create_with_range
+ };
+ use tokei::libraries::errors::Lockup::{
+ STREAM_NOT_CANCELABLE, STREAM_SETTLED, STREAM_NOT_DEPLETED, LOCKUP_UNAUTHORIZED,
+ STREAM_DEPLETED, STREAM_CANCELED, INVALID_SENDER_WITHDRAWAL, WITHDRAW_TO_ZERO_ADDRESS,
+ WITHDRAW_ZERO_AMOUNT, OVERDRAW, NO_PROTOCOL_REVENUE
+ };
+
+ // External Imports
+ use openzeppelin::token::erc20::interface::{
+ IERC20, IERC20Metadata, ERC20ABIDispatcher, ERC20ABIDispatcherTrait
+ };
+ use openzeppelin::token::erc721::erc721::ERC721Component;
+ use openzeppelin::token::erc721::erc721::ERC721Component::InternalTrait;
+ use openzeppelin::introspection::src5::SRC5Component;
+
+ // *************************************************************************
+ // COMPONENTS
+ // *************************************************************************
+ component!(path: ERC721Component, storage: erc721, event: ERC721Event);
+ component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+ #[abi(embed_v0)]
+ impl SRC5Impl = SRC5Component::SRC5Impl;
- use tokei::libraries::helpers;
+
+ #[abi(embed_v0)]
+ impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl;
+ impl ERC721Impl = ERC721Component::ERC721Impl;
+ impl ERC721InternalImpl = ERC721Component::InternalImpl;
// *************************************************************************
// STORAGE
@@ -88,9 +400,20 @@ mod TokeiLockupLinear {
admin: ContractAddress,
next_stream_id: u64,
streams: LegacyMap,
+ nft_descriptor: ContractAddress,
+ flash_fee: u256,
+ is_flash_asset: LegacyMap,
+ protocol_fee: LegacyMap,
+ protocol_revenues: LegacyMap,
+ //Component
+ #[substorage(v0)]
+ src5: SRC5Component::Storage,
+ #[substorage(v0)]
+ erc721: ERC721Component::Storage,
}
- const MAX_FEE: u128 = 100000000000000000;
+ // The maximum fee that can be set for the protocol.
+ const MAX_FEE: u256 = 10; //0.1%
// *************************************************************************
// EVENTS
@@ -100,9 +423,22 @@ mod TokeiLockupLinear {
#[derive(Drop, starknet::Event)]
enum Event {
LockupLinearStreamCreated: LockupLinearStreamCreated,
+ RenounceLockupStream: RenounceLockupStream,
+ SetNFTDescriptor: SetNFTDescriptor,
+ TransferAdmin: TransferAdmin,
+ SetFlashFee: SetFlashFee,
+ SetProtocolFee: SetProtocolFee,
+ ToggleFlashAsset: ToggleFlashAsset,
+ CancelLockupStream: CancelLockupStream,
+ WithdrawFromLockupStream: WithdrawFromLockupStream,
+ ClaimProtocolRevenues: ClaimProtocolRevenues,
+ //Component
+ #[flat]
+ SRC5Event: SRC5Component::Event,
+ #[flat]
+ ERC721Event: ERC721Component::Event,
}
-
#[derive(Drop, starknet::Event)]
struct LockupLinearStreamCreated {
stream_id: u64,
@@ -116,6 +452,71 @@ mod TokeiLockupLinear {
broker: ContractAddress,
}
+ #[derive(Drop, starknet::Event)]
+ struct RenounceLockupStream {
+ stream_id: u64
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct SetNFTDescriptor {
+ admin: ContractAddress,
+ old_nft_descriptor: ContractAddress,
+ new_nft_descriptor: ContractAddress,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct TransferAdmin {
+ old_admin: ContractAddress,
+ new_admin: ContractAddress,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct SetFlashFee {
+ admin: ContractAddress,
+ old_flash_fee: u256,
+ new_flash_fee: u256,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct SetProtocolFee {
+ admin: ContractAddress,
+ asset: ContractAddress,
+ old_protocol_fee: u256,
+ new_protocol_fee: u256,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct ToggleFlashAsset {
+ admin: ContractAddress,
+ asset: ContractAddress,
+ new_flag: bool,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct CancelLockupStream {
+ stream_id: u64,
+ sender: ContractAddress,
+ recipient: ContractAddress,
+ asset: ContractAddress,
+ sender_amount: u256,
+ recipient_amount: u256,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct WithdrawFromLockupStream {
+ stream_id: u64,
+ to: ContractAddress,
+ asset: ContractAddress,
+ amount: u256,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct ClaimProtocolRevenues {
+ admin: ContractAddress,
+ asset: ContractAddress,
+ amount: u256,
+ }
+
// *************************************************************************
// CONSTRUCTOR
// *************************************************************************
@@ -132,171 +533,769 @@ mod TokeiLockupLinear {
self.next_stream_id.write(1);
// Initialize as ERC-721 contract.
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::initializer(
- ref state, 'Tokei Lockup Linear NFT', 'ZW-LOCKUP-LIN', get_contract_address()
- );
+ self.erc721.initializer('Tokei Lockup Linear NFT', 'ZW-LOCKUP-LIN');
+ // Emit the transfer admin event.
+ self.emit(TransferAdmin { old_admin: Zeroable::zero(), new_admin: initial_admin, });
}
-
// *************************************************************************
// EXTERNAL FUNCTIONS
// *************************************************************************
#[external(v0)]
impl TokeiLockupLinear of super::ITokeiLockupLinear {
- /// Create a new stream.
- /// # Arguments
- /// * `sender` - The address streaming the assets, with the ability to cancel the stream.
- /// * `recipient` - The address receiving the assets.
- /// * `total_amount` - The total amount of ERC-20 assets to be paid, including the stream deposit and any potential
- /// fees, all denoted in units of the asset's decimals.
- /// * `asset` - The contract address of the ERC-20 asset used for streaming.
- /// * `cancelable` - Indicates if the stream is cancelable.
- /// * `range` - The range of the stream. Struct containing (i) the stream's start time, (ii) cliff time, and (iii) end time, all as Unix
- /// timestamps.
- /// * `broker` - The broker of the stream. Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the
- /// percentage fee paid to the broker from `totalAmount`.
- fn create_with_range(
- ref self: ContractState,
- sender: ContractAddress,
- recipient: ContractAddress,
- total_amount: u128,
- asset: ContractAddress,
- cancelable: bool,
- range: Range,
- broker: Broker,
- ) -> u64 {
- // Safe Interactions: query the protocol fee. This is safe because it's a known Tokei contract that does
- // not call other unknown contracts.
- // TODO: implement.
+ //////////////////////////////////////////////////////////////////////////
+ //USER-FACING CONSTANT FUNCTIONS
+ //////////////////////////////////////////////////////////////////////////
- let caller = get_caller_address();
- let this = get_contract_address();
+ /// Returns the asset address of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `asset` - The asset address of the stream.
+ fn get_asset(self: @ContractState, stream_id: u64) -> ContractAddress {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).asset
+ }
- // Checks: validate the user-provided parameters.
- // Sanity checks
+ /// Returns the next stream id.
+ /// # Arguments
+ /// -
+ /// # Returns
+ /// * `next_stream_id` - The next stream id.
+ fn get_next_stream_id(self: @ContractState) -> u64 {
+ self.next_stream_id.read()
+ }
- assert(sender != Zeroable::zero(), 'Invalid Sender Address');
- assert(recipient != Zeroable::zero(), 'Invalid Recipient Address');
- assert(broker.account != Zeroable::zero(), 'Invalid broker Address');
- assert(asset != Zeroable::zero(), 'Invalid asset Address');
- assert(total_amount != Zeroable::zero(), 'Invalid total Amount');
+ /// Returns the cliff time of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `cliff_time` - The cliff time of the stream.
+ fn get_cliff_time(self: @ContractState, stream_id: u64) -> u64 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).cliff_time
+ }
- // TODO: Handle MAX_FEE as a constant, with handlign of fixed point numbers.
- // let MAX_FEE = 100000000000000000;
+ /// Returns the deposited amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `deposited_amount` - The deposited amount of the stream.
+ fn get_deposited_amount(self: @ContractState, stream_id: u64) -> u256 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).amounts.deposited
+ }
- // Read the next stream id from storage.
- let stream_id = self.next_stream_id.read();
+ /// Returns the end time of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `end_time` - The end time of the stream.
+ fn get_end_time(self: @ContractState, stream_id: u64) -> u64 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).end_time
+ }
- // Checks: check the fees and calculate the fee amounts.
- let deposited_amount = total_amount - broker.fee;
+ /// Returns the Range of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `range` - The range of the stream.
+ fn get_range(self: @ContractState, stream_id: u64) -> Range {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ let stream = self.streams.read(stream_id);
+ Range { start: stream.start_time, cliff: stream.cliff_time, end: stream.end_time, }
+ }
- let amounts: LockupAmounts = LockupAmounts {
- deposited: deposited_amount, withdrawn: 0, refunded: 0,
- };
+ /// Returns the refundable amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `refundable_amount` - The refundable amount of the stream.
+ fn get_refunded_amount(self: @ContractState, stream_id: u64) -> u256 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).amounts.refunded
+ }
- // Effects: create the stream.
- let stream = LockupLinearStream {
- sender,
- asset,
- start_time: range.start,
- end_time: range.end,
- is_cancelable: cancelable,
- was_canceled: false,
- is_depleted: false,
- amounts,
- };
- self.streams.write(stream_id, stream);
+ /// Returns the status of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `status` - The status of the stream.
+ /// # Note
+ /// * `PENDING` - The stream is pending - 0
+ /// * `STREAMING` - The stream is streaming - 1
+ /// * `CANCELED` - The stream is canceled - 2
+ /// * `SETTLED` - The stream is settled - 3
+ /// * `DEPLETED` - The stream is depleted - 4
+ fn status_of(self: @ContractState, stream_id: u64) -> Status {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ TokeiInternalImpl::_status_of(self, stream_id)
+ }
- // Effects: bump the next stream id.
- self.next_stream_id.write(stream_id + 1);
- // Effects: mint the NFT to the recipient.
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::mint(ref state, recipient, stream_id.into());
+ /// Returns the amount of tokens streamed.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `streamed_amount` - The amount of tokens streamed.
+ fn streamed_amount_of(self: @ContractState, stream_id: u64) -> u256 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ TokeiInternalImpl::_streamed_amount_of(self, stream_id)
+ }
- // Interactions: transfer the deposit and the protocol fee.
- // Casting u128 to u256 for the transfer from function
- let deposit_u256: u256 = amounts.deposited.into();
+ /// Returns the sender address of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_sender(self: @ContractState, stream_id: u64) -> ContractAddress {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).sender
+ }
- IERC20Dispatcher { contract_address: asset }.transfer_from(caller, this, deposit_u256);
+ /// Returns the start time of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_start_time(self: @ContractState, stream_id: u64) -> u64 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).start_time
+ }
- // Interactions: pay the broker fee, if not zero.
- if (broker.fee > 0) {
- let broker_fee_u256: u256 = broker.fee.into();
- IERC20Dispatcher { contract_address: asset }
- .transfer_from(caller, broker.account, broker_fee_u256);
- }
+ /// Returns the stream state for the given stream id.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_stream(self: @ContractState, stream_id: u64) -> LockupLinearStream {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ let stream = self.streams.read(stream_id);
- // Emit an event for the newly created stream.
- self
- .emit(
- LockupLinearStreamCreated {
- stream_id,
- funder: get_caller_address(),
- sender,
- recipient,
- amounts,
- asset,
- cancelable,
- range,
- broker: broker.account,
- }
- );
+ if (TokeiInternalImpl::_status_of(self, stream_id) == Status::SETTLED) {
+ let stream_updated = LockupLinearStream {
+ stream_id: stream.stream_id,
+ sender: stream.sender,
+ asset: stream.asset,
+ recipient: stream.recipient,
+ start_time: stream.start_time,
+ cliff_time: stream.cliff_time,
+ end_time: stream.end_time,
+ is_cancelable: false,
+ was_canceled: stream.was_canceled,
+ is_depleted: stream.is_depleted,
+ is_stream: stream.is_stream,
+ is_transferable: stream.is_transferable,
+ amounts: LockupAmounts {
+ deposited: stream.amounts.deposited,
+ withdrawn: stream.amounts.withdrawn,
+ refunded: stream.amounts.refunded,
+ },
+ };
- // Return the stream id.
- stream_id
+ stream_updated
+ } else {
+ stream
+ }
}
- }
-
- #[generate_trait]
- impl TokeinInternalImpl of TokeiInternalTrait {}
-
- #[external(v0)]
- impl TokeiLockupLinearERC721 of IERC721 {
- fn initializer(
- ref self: ContractState, name_: felt252, symbol_: felt252, admin: ContractAddress
- ) {}
+ /// Returns the withdrawn amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn get_withdrawn_amount(self: @ContractState, stream_id: u64) -> u256 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).amounts.withdrawn
+ }
- fn balance_of(self: @ContractState, account: ContractAddress) -> u128 {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::balance_of(@state, account)
+ /// Returns if the stream is cancelable.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_cancelable(self: @ContractState, stream_id: u64) -> bool {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).is_cancelable
}
- fn owner_of(self: @ContractState, token_id: u128) -> ContractAddress {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::owner_of(@state, token_id)
+ /// Returns if the stream is transferable.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_transferable(self: @ContractState, stream_id: u64) -> bool {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).is_transferable
}
- fn get_approved(self: @ContractState, token_id: u128) -> ContractAddress {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::get_approved(@state, token_id)
+ /// Returns if the stream is depleted.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_depleted(self: @ContractState, stream_id: u64) -> bool {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).is_depleted
+ }
+
+ /// Returns if the stream is canceled.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn is_stream(self: @ContractState, stream_id: u64) -> bool {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).is_stream
+ }
+
+ /// Returns the refundable amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `refundable_amount` - The refundable amount of the stream.
+ fn refundable_amount_of(self: @ContractState, stream_id: u64) -> u256 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ if (self.streams.read(stream_id).is_cancelable
+ && !self.streams.read(stream_id).is_depleted) {
+ self.streams.read(stream_id).amounts.deposited
+ - TokeiInternalImpl::_calculate_streamed_amount(self, stream_id)
+ } else {
+ 0
+ }
+ }
+
+ /// Returns if the stream was canceled.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `true` - If the stream was canceled.
+ fn was_canceled(self: @ContractState, stream_id: u64) -> bool {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.streams.read(stream_id).was_canceled
+ }
+
+ /// Returns the recipient address of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `recipient` - The recipient address of the stream.
+ fn get_recipient(self: @ContractState, stream_id: u64) -> ContractAddress {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self.erc721.owner_of(stream_id.into())
+ }
+
+ /// Returns a bool if the stream was canceled, settled, or depleted.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `true` - If the stream is cold.
+ fn is_cold(self: @ContractState, stream_id: u64) -> bool {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ let status = TokeiInternalImpl::_status_of(self, stream_id);
+ let result = status == Status::CANCELED
+ || status == Status::DEPLETED
+ || status == Status::SETTLED;
+ result
+ }
+
+ /// Returns a bool if the stream is streaming or pending.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `true` - If the stream is warm.
+ fn is_warm(self: @ContractState, stream_id: u64) -> bool {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ let status = TokeiInternalImpl::_status_of(self, stream_id);
+ let result = status == Status::PENDING || status == Status::STREAMING;
+ result
+ }
+
+ /// Returns the withdrawable amount of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// # Returns
+ /// * `withdrawable_amount` - The withdrawable amount of the stream.
+ fn withdrawable_amount_of(self: @ContractState, stream_id: u64) -> u256 {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ self._withdrawable_amount_of(stream_id)
+ }
+
+ /// Returns the amount of protocol revenues for the given asset.
+ /// # Arguments
+ /// * `asset` - The asset to claim the protocol revenues for.
+ /// # Returns
+ /// * `protocol_revenues` - The amount of protocol revenues for the given asset.
+ fn get_protocol_revenues(self: @ContractState, asset: ContractAddress) -> u256 {
+ assert(Zeroable::is_non_zero(asset), 'Invalid asset');
+ self.protocol_revenues.read(asset)
+ }
+
+ /// Returns the protocol fee for the given asset.
+ /// # Arguments
+ /// * `asset` - The asset that has a set protocol fee.
+ /// # Returns
+ /// * `protocol_fee` - The protocol fee for the given asset.
+ fn get_protocol_fee(self: @ContractState, asset: ContractAddress) -> u256 {
+ self.protocol_fee.read(asset)
+ }
+
+ /// Returns the NFT descriptor address.
+ /// # Returns
+ /// * `nft_descriptor` - The NFT descriptor address.
+ fn get_nft_descriptor(self: @ContractState) -> ContractAddress {
+ self.nft_descriptor.read()
+ }
+
+ /// Returns the flash fee
+ /// # Returns
+ /// * `flash_fee` - The flash fee.
+ fn get_flash_fee(self: @ContractState) -> u256 {
+ self.flash_fee.read()
+ }
+
+ /// Returns the admin address.
+ /// # Returns
+ /// * `admin` - The admin address.
+ fn get_admin(self: @ContractState) -> ContractAddress {
+ self.admin.read()
+ }
+
+ /// Returns the streams of the sender.
+ /// # Arguments
+ /// * `sender` - The address of the sender.
+ /// # Returns
+ /// * `streams` - The streams of the sender.
+ fn get_streams_by_sender(
+ self: @ContractState, sender: ContractAddress
+ ) -> Span {
+ let max_stream_id = self.next_stream_id.read();
+ let mut streams: Array = ArrayTrait::new();
+ let mut i = 1; //Since the stream id starts from 1
+ loop {
+ if i >= max_stream_id {
+ break streams.span();
+ }
+ let stream = self.streams.read(i);
+ if stream.sender == sender {
+ streams.append(stream);
+ }
+ i += 1;
+ }
+ }
+
+ /// Returns the streams of the recipient.
+ /// # Arguments
+ /// * `recipient` - The address of the recipient.
+ /// # Returns
+ /// * `streams` - The streams of the recipient.
+ fn get_streams_by_recipient(
+ self: @ContractState, recipient: ContractAddress
+ ) -> Span {
+ let max_stream_id = self.next_stream_id.read();
+ let mut streams: Array = ArrayTrait::new();
+ let mut i = 1; //Since the stream id starts from 1
+ loop {
+ if i >= max_stream_id {
+ break streams.span();
+ }
+ let stream = self.streams.read(i);
+ if stream.recipient == recipient {
+ streams.append(stream);
+ }
+ i += 1;
+ }
+ }
+
+ /// Returns the streams ids of the sender.
+ /// # Arguments
+ /// * `sender` - The address of the sender.
+ /// # Returns
+ /// * `stream_ids` - The stream ids of the sender.
+ fn get_streams_ids_by_sender(self: @ContractState, sender: ContractAddress) -> Span {
+ let max_stream_id = self.next_stream_id.read();
+ let mut stream_ids: Array = ArrayTrait::new();
+ let mut i = 1; // As the stream id starts from 1
+ loop {
+ if i >= max_stream_id {
+ break stream_ids.span();
+ }
+ let stream = self.streams.read(i);
+ if (stream.sender == sender) {
+ stream_ids.append(i);
+ }
+
+ i += 1;
+ }
+ }
+
+ /// Returns the streams ids of the recipient.
+ /// # Arguments
+ /// * `recipient` - The address of the recipient.
+ /// # Returns
+ /// * `stream_ids` - The stream ids of the recipient.
+ fn get_streams_ids_by_recipient(
+ self: @ContractState, recipient: ContractAddress
+ ) -> Span {
+ let max_stream_id = self.next_stream_id.read();
+ let mut stream_ids: Array = ArrayTrait::new();
+ let mut i = 1; // As the stream id starts from 1
+ loop {
+ if i >= max_stream_id {
+ break stream_ids.span();
+ }
+ let stream = self.streams.read(i);
+ if (stream.recipient == recipient) {
+ stream_ids.append(i);
+ }
+
+ i += 1;
+ }
+ }
+
+
+ /// Creates a new stream with a given range.
+ /// # Arguments
+ /// * `sender` - The address streaming the assets, with the ability to cancel the stream.
+ /// * `recipient` - The address receiving the assets.
+ /// * `total_amount` - The total amount of ERC-20 assets to be paid, including the stream deposit and any potential
+ /// * `asset` - The contract address of the ERC-20 asset used for streaming.
+ /// * `cancelable` - Indicates if the stream is cancelable.
+ /// * `transferable` - Indicates if the stream is transferable.
+ /// * `range` - The range of the stream. Struct containing (i) the stream's start time, (ii) cliff time, and (iii) end time, all as Unix
+ /// * `broker` - The broker of the stream. Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the percentage fee paid to the broker from `totalAmount`.
+ /// # Returns
+ /// * `stream_id` - The id of the stream.
+ fn create_with_range(
+ ref self: ContractState,
+ sender: ContractAddress,
+ recipient: ContractAddress,
+ total_amount: u256,
+ asset: ContractAddress,
+ cancelable: bool,
+ transferable: bool,
+ range: Range,
+ broker: Broker,
+ ) -> u64 {
+ TokeiInternalImpl::_create_with_range(
+ ref self,
+ sender,
+ recipient,
+ total_amount,
+ asset,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ )
+ }
+
+ /// Creates a new stream with a given duration.
+ /// # Arguments
+ /// * `sender` - The address streaming the assets, with the ability to cancel the stream.
+ /// * `recipient` - The address receiving the assets.
+ /// * `total_amount` - The total amount of ERC-20 assets to be paid, including the stream deposit and any potential
+ /// * `asset` - The contract address of the ERC-20 asset used for streaming.
+ /// * `cancelable` - Indicates if the stream is cancelable.
+ /// * `transferable` - Indicates if the stream is transferable.
+ /// * `duration` - The duration of the stream. Struct containing (i) the stream's cliff period, (ii) total duration
+ /// * `broker` - The broker of the stream. Struct containing (i) the address of the broker assisting in creating the stream, and (ii) the percentage fee paid to the broker from `totalAmount`.
+ /// # Returns
+ /// * `stream_id` - The id of the stream.
+ fn create_with_duration(
+ ref self: ContractState,
+ sender: ContractAddress,
+ recipient: ContractAddress,
+ total_amount: u256,
+ asset: ContractAddress,
+ cancelable: bool,
+ transferable: bool,
+ duration: Durations,
+ broker: Broker
+ ) -> u64 {
+ let start_time = get_block_timestamp();
+ let range = Range {
+ start: start_time,
+ cliff: start_time + duration.cliff,
+ end: start_time + duration.total,
+ };
+
+ let stream_id = TokeiInternalImpl::_create_with_range(
+ ref self,
+ sender,
+ recipient,
+ total_amount,
+ asset,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+
+ stream_id
+ }
+
+ /// Burns the NFT token of the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn burn_token(ref self: ContractState, stream_id: u64) {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ assert(self.is_depleted(stream_id), STREAM_NOT_DEPLETED);
+ assert(
+ TokeiInternalImpl::_is_caller_stream_recipient_or_approved(@self, stream_id),
+ LOCKUP_UNAUTHORIZED
+ );
+
+ self.erc721._burn(stream_id.into());
+ }
+
+ /// Cancels the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn cancel(ref self: ContractState, stream_id: u64) {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ assert(!self.is_depleted(stream_id), STREAM_DEPLETED);
+ assert(!self.was_canceled(stream_id), STREAM_CANCELED);
+ let value = TokeiInternalImpl::_is_caller_stream_sender(@self, stream_id);
+ assert(
+ value || get_caller_address() == self.get_recipient(stream_id), LOCKUP_UNAUTHORIZED
+ );
+
+ TokeiInternalImpl::_cancel(ref self, stream_id);
+ }
+
+ /// Cancels multiple streams.
+ /// # Arguments
+ /// * `stream_ids` - The ids of the streams.
+ fn cancel_multiple(ref self: ContractState, stream_ids: Span) {
+ let count = stream_ids.len();
+ let mut i = 0;
+ loop {
+ if i >= count {
+ break;
+ }
+ let stream_id = *stream_ids.at(i);
+ self.cancel(stream_id);
+ i += 1;
+ };
+ }
+
+ /// Renounces the stream.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ fn renounce(ref self: ContractState, stream_id: u64) {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ let status = self._status_of(stream_id);
+ assert(status != Status::DEPLETED, STREAM_DEPLETED);
+ assert(status != Status::CANCELED, STREAM_CANCELED);
+ assert(status != Status::SETTLED, STREAM_SETTLED);
+ assert(self._is_caller_stream_sender(stream_id), LOCKUP_UNAUTHORIZED);
+
+ self._renounce(stream_id);
+ }
+
+ /// Sets the NFT descriptor.
+ /// # Arguments
+ /// * `nft_descriptor` - The NFT descriptor.
+ fn set_nft_descriptor(ref self: ContractState, nft_descriptor: ContractAddress) {
+ assert(Zeroable::is_non_zero(nft_descriptor), 'Invalid nft descriptor');
+ self.assert_only_admin();
+ let old_nft_descriptor = self.nft_descriptor.read();
+ self.nft_descriptor.write(nft_descriptor);
+
+ self
+ .emit(
+ SetNFTDescriptor {
+ admin: self.admin.read(),
+ old_nft_descriptor: old_nft_descriptor,
+ new_nft_descriptor: nft_descriptor,
+ }
+ );
+ }
+
+ /// Withdraws the stream's assets.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// * `to` - The address to withdraw the assets to.
+ /// * `amount` - The amount of assets to withdraw.
+ fn withdraw(ref self: ContractState, stream_id: u64, to: ContractAddress, amount: u256) {
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ assert(Zeroable::is_non_zero(amount), WITHDRAW_ZERO_AMOUNT);
+ assert(to != Zeroable::zero(), WITHDRAW_TO_ZERO_ADDRESS);
+ assert(!self.is_depleted(stream_id), STREAM_DEPLETED);
+
+ let value = TokeiInternalImpl::_is_caller_stream_sender(@self, stream_id);
+
+ assert(
+ value
+ || TokeiInternalImpl::_is_caller_stream_recipient_or_approved(@self, stream_id),
+ LOCKUP_UNAUTHORIZED
+ );
+
+ let recipient = self.get_recipient(stream_id);
+ assert(to == recipient, INVALID_SENDER_WITHDRAWAL);
+
+ TokeiInternalImpl::_withdraw(ref self, stream_id, to, amount);
+ }
+
+ /// Withdraws maximum amount of the stream's assets.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// * `new_recipient` - The address to withdraw the assets to.
+ fn withdraw_max(ref self: ContractState, stream_id: u64, to: ContractAddress) {
+ self.withdraw(stream_id, to, self.withdrawable_amount_of(stream_id));
+ }
+
+ /// Withdraws maximum amount of the stream's assets and transfers the NFT to a new recipient.
+ /// # Arguments
+ /// * `stream_id` - The id of the stream.
+ /// * `new_recipient` - The address to withdraw the assets to.
+ fn withdraw_max_and_transfer(
+ ref self: ContractState, stream_id: u64, new_recipient: ContractAddress
+ ) {
+ let current_recipient = self.get_recipient(stream_id);
+ assert(self.is_transferable(stream_id), 'Stream is not transferable');
+ assert(Zeroable::is_non_zero(stream_id), 'Invalid stream id');
+ assert(Zeroable::is_non_zero(new_recipient), 'Invalid new_recipient');
+ assert(get_caller_address() == current_recipient, LOCKUP_UNAUTHORIZED);
+ let withdrawable_amount = self.withdrawable_amount_of(stream_id);
+ if (withdrawable_amount > 0) {
+ self.withdraw(stream_id, current_recipient, withdrawable_amount);
+ }
+
+ self.erc721.transfer_from(current_recipient, new_recipient, stream_id.into());
+ }
+
+ /// Withdraws multiple streams' assets.
+ /// # Arguments
+ /// * `stream_ids` - The ids of the streams.
+ /// * `to` - The address to withdraw the assets to.
+ /// * `amounts` - The amounts of assets to withdraw.
+ fn withdraw_multiple(
+ ref self: ContractState, stream_ids: Span, to: ContractAddress, amounts: Span
+ ) {
+ let stream_ids_count = stream_ids.len();
+ let amounts_count = amounts.len();
+ assert(stream_ids_count == amounts_count, 'Invalid array lengths');
+ let mut i = 0;
+ loop {
+ if i >= stream_ids_count {
+ break;
+ }
+ let stream_id = *stream_ids.at(i);
+ let amount = *amounts.at(i);
+ self.withdraw(stream_id, to, amount);
+ i += 1;
+ };
+ }
+
+ /// Sets the flash fee.
+ /// # Arguments
+ /// * `new_flash_fee` - The new flash fee.
+ fn set_flash_fee(ref self: ContractState, new_flash_fee: u256) {
+ self.assert_only_admin();
+ let old_fee = self.flash_fee.read();
+ self.flash_fee.write(new_flash_fee);
+
+ self
+ .emit(
+ SetFlashFee {
+ admin: self.admin.read(),
+ old_flash_fee: old_fee,
+ new_flash_fee: new_flash_fee,
+ }
+ );
+ }
+
+ /// Sets the protocol fee.
+ /// # Arguments
+ /// * `asset` - The asset to set the protocol fee for.
+ /// * `new_protocol_fee` - The new protocol fee.
+ fn set_protocol_fee(
+ ref self: ContractState, asset: ContractAddress, new_protocol_fee: u256
+ ) {
+ self.assert_only_admin();
+ let old_protocol_fee = self.protocol_fee.read(asset);
+ self.protocol_fee.write(asset, new_protocol_fee);
+
+ self
+ .emit(
+ SetProtocolFee {
+ admin: self.admin.read(),
+ asset: asset,
+ old_protocol_fee: old_protocol_fee,
+ new_protocol_fee: new_protocol_fee,
+ }
+ );
+ }
+
+ /// Toggles the flash asset flag.
+ /// # Arguments
+ /// * `asset` - The asset to toggle the flash asset flag for.
+ fn toggle_flash_assets(ref self: ContractState, asset: ContractAddress) {
+ self.assert_only_admin();
+ let old_flag = self.is_flash_asset.read(asset);
+ self.is_flash_asset.write(asset, !old_flag);
+
+ self
+ .emit(
+ ToggleFlashAsset {
+ admin: self.admin.read(), asset: asset, new_flag: !old_flag,
+ }
+ );
+ }
+
+ /// Transfers the admin.
+ /// # Arguments
+ /// * `new_admin` - The new admin.
+ fn transfer_admin(ref self: ContractState, new_admin: ContractAddress) {
+ self.assert_only_admin();
+ let old_admin = self.admin.read();
+ self.admin.write(new_admin);
+
+ self.emit(TransferAdmin { old_admin: old_admin, new_admin: new_admin, });
+ }
+
+ /// Claims the protocol revenues for the given asset.
+ /// # Arguments
+ /// * `asset` - The asset to claim the protocol revenues for.
+ fn claim_protocol_revenues(ref self: ContractState, asset: ContractAddress) {
+ self.assert_only_admin();
+ let protocol_revenues = self.protocol_revenues.read(asset);
+ assert(protocol_revenues > 0, NO_PROTOCOL_REVENUE);
+
+ self.protocol_revenues.write(asset, 0);
+
+ ERC20ABIDispatcher { contract_address: asset }
+ .transfer(self.admin.read(), protocol_revenues.into());
+
+ self
+ .emit(
+ ClaimProtocolRevenues {
+ admin: self.admin.read(), asset: asset, amount: protocol_revenues
+ }
+ );
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl SnakeTokeiLockupLinearERC721 of ITokeiLockupLinearERC721Snake {
+ fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
+ self.erc721.balance_of(account)
+ }
+
+ fn owner_of(self: @ContractState, token_id: u128) -> ContractAddress {
+ self.erc721.owner_of(token_id.into())
+ }
+
+ fn get_approved(self: @ContractState, token_id: u128) -> ContractAddress {
+ self.erc721.get_approved(token_id.into())
}
fn is_approved_for_all(
self: @ContractState, owner: ContractAddress, operator: ContractAddress
) -> bool {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::is_approved_for_all(@state, owner, operator)
+ self.erc721.is_approved_for_all(owner, operator)
}
fn approve(ref self: ContractState, to: ContractAddress, token_id: u128) {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::approve(ref state, to, token_id)
+ self.erc721.approve(to, token_id.into());
}
fn set_approval_for_all(
ref self: ContractState, operator: ContractAddress, approved: bool
) {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::set_approval_for_all(ref state, operator, approved)
+ self.erc721.set_approval_for_all(operator, approved);
}
fn transfer_from(
ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u128
) {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::transfer_from(ref state, from, to, token_id)
+ self.erc721.transfer_from(from, to, token_id.into());
}
fn safe_transfer_from(
@@ -306,13 +1305,474 @@ mod TokeiLockupLinear {
token_id: u128,
data: Span
) {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::safe_transfer_from(ref state, from, to, token_id, data)
+ self.erc721.safe_transfer_from(from, to, token_id.into(), data);
}
+ }
- fn mint(ref self: ContractState, to: ContractAddress, token_id: u128) {
- let mut state: ERC721::ContractState = ERC721::unsafe_new_contract_state();
- IERC721::mint(ref state, to, token_id)
+ #[abi(embed_v0)]
+ impl CamelITokeiLockupLinearERC721 of ITokeiLockupLinearERC721Camel {
+ fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 {
+ self.erc721.balance_of(account)
+ }
+ fn ownerOf(self: @ContractState, token_id: u128) -> ContractAddress {
+ self.erc721.owner_of(token_id.into())
+ }
+ fn getApproved(self: @ContractState, token_id: u128) -> ContractAddress {
+ self.erc721.get_approved(token_id.into())
+ }
+ fn isApprovedForAll(
+ self: @ContractState, owner: ContractAddress, operator: ContractAddress
+ ) -> bool {
+ self.erc721.is_approved_for_all(owner, operator)
+ }
+ fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) {
+ self.erc721.set_approval_for_all(operator, approved);
+ }
+ fn transferFrom(
+ ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u128
+ ) {
+ self.erc721.transfer_from(from, to, token_id.into());
+ }
+ fn safeTransferFrom(
+ ref self: ContractState,
+ from: ContractAddress,
+ to: ContractAddress,
+ token_id: u128,
+ data: Span
+ ) {
+ self.erc721.safe_transfer_from(from, to, token_id.into(), data);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ ///INTERNAL CONSTANT FUNCTIONS
+ ///////////////////////////////////////////////////////////////////////////
+
+ #[generate_trait]
+ impl TokeiInternalImpl of TokeiInternalTrait {
+ // Assertion that caller is the admin.
+ fn assert_only_admin(self: @ContractState) {
+ assert(get_caller_address() == self.admin.read(), LOCKUP_UNAUTHORIZED);
+ }
+
+ // Calculates the streamed amount of the stream.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ // # Returns
+ // * `streamed_amount` - The streamed amount of the stream.
+ fn _calculate_streamed_amount(self: @ContractState, stream_id: u64) -> u256 {
+ let cliff_time = self.streams.read(stream_id).cliff_time;
+ let current_time = get_block_timestamp();
+
+ // If the cliff time is in the future, return zero.
+ if (current_time < cliff_time) {
+ return 0;
+ }
+
+ // If the end time is not in the future, return the deposited amount.
+ let end_time = self.streams.read(stream_id).end_time;
+
+ if (current_time >= end_time) {
+ return self.streams.read(stream_id).amounts.deposited;
+ }
+
+ let start_time = self.streams.read(stream_id).start_time;
+
+ let elapsed_time = current_time - start_time;
+
+ let total_time = end_time - start_time;
+
+ // Divide the elapsed time by the stream's total duration.
+ let elapsed_time_percentage = scaled_down_div(elapsed_time, total_time);
+
+ let deposited_amount = self.streams.read(stream_id).amounts.deposited;
+ // Convert the percentage to a felt252 and then to a u128.
+ let elapsed_time_percentage_felt: felt252 = elapsed_time_percentage.into();
+ let elapsed_time_percentage_u256: u256 = elapsed_time_percentage_felt.into();
+
+ // Multiply the deposited amount by the percentage.
+ let _streamed_amount = deposited_amount * elapsed_time_percentage_u256;
+ let streamed_amount = _streamed_amount / 100;
+
+ // Although the streamed amount should never exceed the deposited amount, this condition is checked
+ // without asserting to avoid locking funds in case of a bug. If this situation occurs, the withdrawn
+ // amount is considered to be the streamed amount, and the stream is effectively frozen.
+ if (streamed_amount > deposited_amount) {
+ return self.streams.read(stream_id).amounts.withdrawn;
+ }
+
+ return streamed_amount;
+ }
+
+ // Retuns the status of the stream.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ // # Returns
+ // * `status` - The status of the stream.
+ fn _status_of(self: @ContractState, stream_id: u64) -> Status {
+ if (self.streams.read(stream_id).is_depleted) {
+ return Status::DEPLETED;
+ } else if (self.streams.read(stream_id).was_canceled) {
+ return Status::CANCELED;
+ }
+
+ if (get_block_timestamp() < self.streams.read(stream_id).start_time) {
+ return Status::PENDING;
+ }
+
+ if (TokeiInternalImpl::_calculate_streamed_amount(self, stream_id) < self
+ .streams
+ .read(stream_id)
+ .amounts
+ .deposited) {
+ return Status::STREAMING;
+ } else {
+ return Status::SETTLED;
+ }
+ }
+
+ // Withdraws the stream's deposited amount to the recipient.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ // * `to` - The recipient of the withdrawn amount.
+ // * `amount` - The amount to withdraw.
+ fn _withdraw(ref self: ContractState, stream_id: u64, to: ContractAddress, amount: u256) {
+ // Calculates the amount that can be withdrawn.
+ let withdrawable_amount = self._withdrawable_amount_of(stream_id);
+ assert(amount <= withdrawable_amount, OVERDRAW);
+ let stream = self.streams.read(stream_id);
+ let stream_updated = LockupLinearStream {
+ stream_id: stream.stream_id,
+ sender: stream.sender,
+ asset: stream.asset,
+ recipient: stream.recipient,
+ start_time: stream.start_time,
+ cliff_time: stream.cliff_time,
+ end_time: stream.end_time,
+ is_cancelable: stream.is_cancelable,
+ was_canceled: stream.was_canceled,
+ is_depleted: stream.is_depleted,
+ is_stream: stream.is_stream,
+ is_transferable: stream.is_transferable,
+ amounts: LockupAmounts {
+ deposited: stream.amounts.deposited,
+ withdrawn: stream.amounts.withdrawn + amount,
+ refunded: stream.amounts.refunded,
+ },
+ };
+ self.streams.write(stream_id, stream_updated);
+
+ let amounts = self.streams.read(stream_id).amounts;
+
+ if (amounts.withdrawn >= amounts.deposited - amounts.refunded) {
+ let _stream_updated = LockupLinearStream {
+ stream_id: stream.stream_id,
+ sender: stream.sender,
+ asset: stream.asset,
+ recipient: stream.recipient,
+ start_time: stream.start_time,
+ cliff_time: stream.cliff_time,
+ end_time: stream.end_time,
+ is_cancelable: false,
+ was_canceled: stream.was_canceled,
+ is_depleted: true,
+ is_stream: stream.is_stream,
+ is_transferable: stream.is_transferable,
+ amounts: LockupAmounts {
+ deposited: stream.amounts.deposited,
+ withdrawn: stream.amounts.withdrawn,
+ refunded: stream.amounts.refunded,
+ },
+ };
+ self.streams.write(stream_id, _stream_updated);
+ }
+
+ let asset = stream.asset;
+ ERC20ABIDispatcher { contract_address: asset }.transfer(to, amount.into());
+ self.emit(WithdrawFromLockupStream { stream_id, to, asset, amount, });
+ }
+
+ // Renounces the stream.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ fn _renounce(ref self: ContractState, stream_id: u64) {
+ let stream = self.streams.read(stream_id);
+ // Checks: the stream is cancelable.
+ assert(stream.is_cancelable, STREAM_NOT_CANCELABLE);
+ let stream_updated = LockupLinearStream {
+ stream_id: stream.stream_id,
+ sender: stream.sender,
+ asset: stream.asset,
+ recipient: stream.recipient,
+ start_time: stream.start_time,
+ cliff_time: stream.cliff_time,
+ end_time: stream.end_time,
+ is_cancelable: false,
+ was_canceled: stream.was_canceled,
+ is_depleted: stream.is_depleted,
+ is_stream: stream.is_stream,
+ is_transferable: stream.is_transferable,
+ amounts: LockupAmounts {
+ deposited: stream.amounts.deposited,
+ withdrawn: stream.amounts.withdrawn,
+ refunded: stream.amounts.refunded,
+ },
+ };
+ // renounce the stream by making it not cancelable.
+ self.streams.write(stream_id, stream_updated);
+ // Emit an event for the renounced stream.
+ self.emit(RenounceLockupStream { stream_id });
+ }
+
+ // Cancels the stream.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ fn _cancel(ref self: ContractState, stream_id: u64) {
+ // Calculates the streamed amount of the stream.
+ let streamed_amount = TokeiInternalImpl::_calculate_streamed_amount(@self, stream_id);
+
+ let amounts = self.streams.read(stream_id).amounts;
+ // Checks: if the amount deposited is greater than the streamed amount.
+ assert(streamed_amount < amounts.deposited, STREAM_SETTLED);
+ // Checks: if the stream is cancelable.
+ assert(self.streams.read(stream_id).is_cancelable, STREAM_NOT_CANCELABLE);
+
+ // Calculates the refundable amount of the stream.
+ let sender_amount = amounts.deposited - streamed_amount;
+ let recipient_amount = streamed_amount - amounts.withdrawn;
+ let stream = self.streams.read(stream_id);
+
+ if (recipient_amount == 0) {
+ let stream_updated = LockupLinearStream {
+ stream_id: stream.stream_id,
+ sender: stream.sender,
+ asset: stream.asset,
+ recipient: stream.recipient,
+ start_time: stream.start_time,
+ cliff_time: stream.cliff_time,
+ end_time: stream.end_time,
+ is_cancelable: false,
+ was_canceled: true,
+ is_depleted: true,
+ is_stream: stream.is_stream,
+ is_transferable: stream.is_transferable,
+ amounts: LockupAmounts {
+ deposited: stream.amounts.deposited,
+ withdrawn: stream.amounts.withdrawn,
+ refunded: sender_amount,
+ },
+ };
+
+ self.streams.write(stream_id, stream_updated);
+ } else {
+ let stream_updated = LockupLinearStream {
+ stream_id: stream.stream_id,
+ sender: stream.sender,
+ asset: stream.asset,
+ recipient: stream.recipient,
+ start_time: stream.start_time,
+ cliff_time: stream.cliff_time,
+ end_time: stream.end_time,
+ is_cancelable: false,
+ was_canceled: true,
+ is_depleted: stream.is_depleted,
+ is_stream: stream.is_stream,
+ is_transferable: stream.is_transferable,
+ amounts: LockupAmounts {
+ deposited: stream.amounts.deposited,
+ withdrawn: stream.amounts.withdrawn,
+ refunded: sender_amount,
+ },
+ };
+
+ self.streams.write(stream_id, stream_updated);
+ }
+
+ let sender = stream.sender;
+ let recipient = self.get_recipient(stream_id);
+ // Interactions: transfer the refundable amount from the protocol's contract to the stream sender.
+ ERC20ABIDispatcher { contract_address: stream.asset }
+ .transfer(sender, sender_amount.into());
+
+ // Emit an event for the canceled stream.
+ self
+ .emit(
+ CancelLockupStream {
+ stream_id,
+ sender,
+ recipient,
+ asset: stream.asset,
+ sender_amount,
+ recipient_amount,
+ }
+ );
+ }
+
+ // Returns the withdrawable amount of the stream.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ // # Returns
+ // * The withdrawable amount of the stream.
+ fn _withdrawable_amount_of(self: @ContractState, stream_id: u64) -> u256 {
+ TokeiInternalImpl::_streamed_amount_of(self, stream_id)
+ - self.streams.read(stream_id).amounts.withdrawn
+ }
+
+
+ // Returns the streamed amount of the stream.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ // # Returns
+ // * The streamed amount of the stream.
+ fn _streamed_amount_of(self: @ContractState, stream_id: u64) -> u256 {
+ let stream = self.streams.read(stream_id);
+ let amounts = stream.amounts;
+
+ if (stream.is_depleted) {
+ return amounts.withdrawn;
+ } else if (stream.was_canceled) {
+ return amounts.deposited - amounts.refunded;
+ }
+
+ TokeiInternalImpl::_calculate_streamed_amount(self, stream_id)
+ }
+
+ // Checks if the caller is the stream sender.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ // # Returns
+ // * `true` if the caller is the stream sender.
+ fn _is_caller_stream_sender(self: @ContractState, stream_id: u64) -> bool {
+ let stream = self.streams.read(stream_id);
+ get_caller_address() == stream.sender
+ }
+
+
+ // Creates a new stream with the given parameters.
+ // # Arguments
+ // * `sender` - The address of the stream sender.
+ // * `recipient` - The address of the stream recipient.
+ // * `total_amount` - The total amount of the stream.
+ // * `asset` - The address of the asset to be streamed.
+ // * `cancelable` - Whether the stream is cancelable.
+ // * `transferable` - Whether the stream is transferable.
+ // * `range` - The range of the stream.
+ // * `broker` - The broker of the stream.
+ // # Returns
+ // * The stream id of the newly created stream.
+ fn _create_with_range(
+ ref self: ContractState,
+ sender: ContractAddress,
+ recipient: ContractAddress,
+ total_amount: u256,
+ asset: ContractAddress,
+ cancelable: bool,
+ transferable: bool,
+ range: Range,
+ broker: Broker,
+ ) -> u64 {
+ let protocol_fee = self.protocol_fee.read(asset);
+
+ let create_amounts = check_and_calculate_fees(
+ total_amount, protocol_fee, broker.fee, MAX_FEE
+ );
+
+ // Checks: check the fees and calculate the fee amounts.
+ check_create_with_range(create_amounts.deposit, range);
+
+ let caller = get_caller_address();
+ let this = get_contract_address();
+
+ // Checks: validate the user-provided parameters.
+ // Sanity checks
+
+ assert(sender != Zeroable::zero(), 'Invalid Sender Address');
+ assert(recipient != Zeroable::zero(), 'Invalid Recipient Address');
+ assert(broker.account != Zeroable::zero(), 'Invalid broker Address');
+ assert(asset != Zeroable::zero(), 'Invalid asset Address');
+ assert(total_amount != Zeroable::zero(), 'Invalid total Amount');
+
+ // Read the next stream id from storage.
+ let stream_id = self.next_stream_id.read();
+
+ // Creating the LockupAmounts struct
+ let amounts: LockupAmounts = LockupAmounts {
+ deposited: create_amounts.deposit, withdrawn: 0, refunded: 0,
+ };
+
+ // Effects: create the stream.
+ let stream = LockupLinearStream {
+ stream_id: stream_id,
+ sender,
+ asset,
+ recipient,
+ start_time: range.start,
+ cliff_time: range.cliff,
+ end_time: range.end,
+ is_cancelable: cancelable,
+ was_canceled: false,
+ is_depleted: false,
+ is_stream: true,
+ is_transferable: transferable,
+ amounts,
+ };
+ self.streams.write(stream_id, stream);
+
+ // Effects: bump the next stream id.
+ self.next_stream_id.write(stream_id + 1);
+
+ // Effects: update the protocol revenues.
+ let protocol_revenue = self.protocol_revenues.read(asset) + create_amounts.protocol_fee;
+ self.protocol_revenues.write(asset, protocol_revenue);
+
+ let res = self.protocol_revenues.read(asset);
+
+ // Effects: mint the NFT to the recipient.
+ self.erc721._mint(recipient, stream_id.into());
+
+ // Interactions: transfer the deposited amount from the caller to the protocol's contract.
+ ERC20ABIDispatcher { contract_address: asset }
+ .transfer_from(caller, this, amounts.deposited);
+
+ // Interactions: pay the broker fee, if not zero.
+ if (broker.fee > 0) {
+ ERC20ABIDispatcher { contract_address: asset }
+ .transfer_from(caller, broker.account, create_amounts.broker_fee);
+ }
+
+ // Emit an event for the newly created stream.
+ self
+ .emit(
+ LockupLinearStreamCreated {
+ stream_id,
+ funder: get_caller_address(),
+ sender,
+ recipient,
+ amounts,
+ asset,
+ cancelable,
+ range,
+ broker: broker.account,
+ }
+ );
+
+ // Return the stream id.
+ stream_id
+ }
+
+ // Checks if the caller is the stream recipient or an approved operator.
+ // # Arguments
+ // * `stream_id` - The id of the stream.
+ // # Returns
+ // * `true` if the caller is the stream recipient or an approved operator.
+ fn _is_caller_stream_recipient_or_approved(self: @ContractState, stream_id: u64) -> bool {
+ let stream_id_u256: u256 = stream_id.into();
+ let recipient = self.get_recipient(stream_id);
+
+ return get_caller_address() == recipient
+ || self.erc721.get_approved(stream_id_u256) == get_caller_address()
+ || self.erc721.is_approved_for_all(recipient, get_caller_address());
}
}
}
+
diff --git a/src/lib.cairo b/src/lib.cairo
index 2f72a38..57cf76e 100644
--- a/src/lib.cairo
+++ b/src/lib.cairo
@@ -1,6 +1,7 @@
/// Core module.
mod core {
mod lockup_linear;
+ mod interface;
}
/// Module containing types for the system.
@@ -15,9 +16,16 @@ mod libraries {
mod errors;
}
-/// Module containing tokens implementations.
-/// TODO: remove and use OpenZeppelin dependency when it's ready.
-mod tokens {
- mod erc20;
- mod erc721;
+/// Module containing tests.
+mod tests {
+ #[cfg(test)]
+ mod test_lockup_linear;
+ mod utils {
+ mod defaults;
+ mod utils;
+ }
+ mod mocks {
+ mod erc20;
+ }
}
+
diff --git a/src/libraries/errors.cairo b/src/libraries/errors.cairo
index 3851e78..fd8e435 100644
--- a/src/libraries/errors.cairo
+++ b/src/libraries/errors.cairo
@@ -1,6 +1,23 @@
mod Lockup {
const PROTOCOL_FEE_TOO_HIGH: felt252 = 'protocol_fee_too_high';
const BROKER_FEE_TOO_HIGH: felt252 = 'broker_fee_too_high';
+ const STREAM_NOT_CANCELABLE: felt252 = 'stream_not_cancelable';
+ const STREAM_CANCELED: felt252 = 'stream is canceled';
+ const STREAM_SETTLED: felt252 = 'stream_settled';
+ const STREAM_NOT_DEPLETED: felt252 = 'stream has not depleted';
+ const STREAM_DEPLETED: felt252 = 'stream has depleted';
+ const LOCKUP_UNAUTHORIZED: felt252 = 'lockup_unauthorized';
+ const INVALID_SENDER_WITHDRAWAL: felt252 = 'invalid sender withdrawal';
+ const WITHDRAW_TO_ZERO_ADDRESS: felt252 = 'withdraw to zero address';
+ const WITHDRAW_ZERO_AMOUNT: felt252 = 'withdraw zero amount';
+ const OVERDRAW: felt252 = 'Amount more than available';
+ const DEPOSIT_AMOUNT_ZERO: felt252 = 'deposit amount is zero';
+ const START_TIME_GREAT_THAN_CLIFF_TIME: felt252 = 'start time > cliff time';
+
+ const TOTAL_AMOUNT_TOO_LOW: felt252 = 'total amount too low';
+ const CLIFF_TIME_LESS_THAN_END_TIME: felt252 = 'cliff time < end time';
+ const CURRENT_TIME_GREATER_THAN_END_TIME: felt252 = 'current time > end time';
+ const NO_PROTOCOL_REVENUE: felt252 = 'No protocol revenues to claim';
fn protocol_fee_too_high(protocol_fee: u128, max_fee: u128) {
panic(array![PROTOCOL_FEE_TOO_HIGH, protocol_fee.into(), max_fee.into()])
diff --git a/src/libraries/helpers.cairo b/src/libraries/helpers.cairo
index 41ca086..726dd57 100644
--- a/src/libraries/helpers.cairo
+++ b/src/libraries/helpers.cairo
@@ -1,49 +1,87 @@
+use core::debug::PrintTrait;
// *************************************************************************
// IMPORTS
// *************************************************************************
// Core lib imports.
use zeroable::Zeroable;
-
+use starknet::get_block_timestamp;
// Local imports.
use tokei::types::lockup::CreateAmounts;
-use tokei::libraries::errors;
+use tokei::types::lockup_linear::Range;
+use tokei::libraries::errors::Lockup::{
+ DEPOSIT_AMOUNT_ZERO, BROKER_FEE_TOO_HIGH, PROTOCOL_FEE_TOO_HIGH, TOTAL_AMOUNT_TOO_LOW,
+ START_TIME_GREAT_THAN_CLIFF_TIME, CLIFF_TIME_LESS_THAN_END_TIME,
+ CURRENT_TIME_GREATER_THAN_END_TIME
+};
+
+const BPS: u256 = 10_000; // 100% = 10_000 bps
+
+trait PercentageMath {
+ fn percent_mul(self: u256, other: u256) -> u256;
+}
+
+impl PercentageMathImpl of PercentageMath {
+ fn percent_mul(self: u256, other: u256) -> u256 {
+ self * other / BPS
+ }
+}
+
+fn scaled_down_div(lhs: u64, rhs: u64) -> u64 {
+ let SCALE_FACTOR = 100;
+ let scaling_val = lhs * SCALE_FACTOR;
+
+ let half_b = rhs / 2_u64;
+
+ let scaled_a_rounded = scaling_val + half_b;
+
+ let res = scaled_a_rounded / rhs;
+
+ // let res = U64DivRem(a, b, c, Rounding::Up);
+
+ res
+}
//Checks that neither fee is greater than `max_fee`, and then calculates the protocol fee amount, the
/// broker fee amount, and the deposit amount from the total amount.
fn check_and_calculate_fees(
- total_amount: u128, protocol_fee: u128, broker_fee: u128, max_fee: u128
+ total_amount: u256, protocol_fee: u256, broker_fee: u256, max_fee: u256
) -> CreateAmounts {
- // TODO: Handle fixed point arithmetic everywhere.
-
// When the total amount is zero, the fees are also zero.
if (total_amount.is_zero()) {
- return Zeroable::zero();
+ return CreateAmounts { protocol_fee: 0, broker_fee: 0, deposit: 0 };
}
// Checks: the protocol fee is not greater than `max_fee`.
- if (protocol_fee > max_fee) {
- errors::Lockup::protocol_fee_too_high(protocol_fee, max_fee);
- }
+
+ assert(protocol_fee < max_fee, PROTOCOL_FEE_TOO_HIGH);
// Checks: the broker fee is not greater than `max_fee`.
- if (broker_fee > max_fee) {
- errors::Lockup::broker_fee_too_high(broker_fee, max_fee);
- }
+ assert(broker_fee < max_fee, BROKER_FEE_TOO_HIGH);
// Calculate the protocol fee amount.
- let protocol_fee = total_amount * protocol_fee;
+ let protocol_fees = total_amount.percent_mul(protocol_fee);
// Calculate the broker fee amount.
- let broker_fee = total_amount * broker_fee;
+ let broker_fees = total_amount.percent_mul(broker_fee);
// Assert that the total amount is strictly greater than the sum of the protocol fee amount and the
// broker fee amount.
- assert(total_amount > protocol_fee + broker_fee, 'total_amount_too_low');
+ assert(total_amount > protocol_fees + broker_fees, TOTAL_AMOUNT_TOO_LOW);
// Calculate the deposit amount (the amount to stream, net of fees).
- let deposit = total_amount - protocol_fee - broker_fee;
+ let deposit = total_amount - protocol_fees - broker_fees;
// Return the amounts.
- CreateAmounts { protocol_fee, broker_fee, deposit }
+ CreateAmounts { protocol_fee: protocol_fees, broker_fee: broker_fees, deposit: deposit }
}
+
+fn check_create_with_range(deposit_amount: u256, range: Range) {
+ assert(deposit_amount > 0, DEPOSIT_AMOUNT_ZERO);
+ assert(range.cliff > range.start, START_TIME_GREAT_THAN_CLIFF_TIME);
+ assert(range.end > range.cliff, CLIFF_TIME_LESS_THAN_END_TIME);
+
+ let current_time = get_block_timestamp();
+ assert(current_time < range.end, CURRENT_TIME_GREATER_THAN_END_TIME)
+}
+
diff --git a/src/scripts/admin_interaction.ts b/src/scripts/admin_interaction.ts
new file mode 100644
index 0000000..3330fd3
--- /dev/null
+++ b/src/scripts/admin_interaction.ts
@@ -0,0 +1,323 @@
+import {
+ constants,
+ Provider,
+ Contract,
+ Account,
+ json,
+ shortString,
+ RpcProvider,
+ hash,
+ CallData,
+ cairo,
+ Uint256,
+} from "starknet";
+import fs from "fs";
+import readline from "readline";
+import { ethers } from "ethers";
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+});
+
+function ask(question: string): Promise {
+ return new Promise((resolve) => rl.question(question, resolve));
+}
+
+async function main() {
+ let lastTransactionHash = "";
+ while (true) {
+ // List of functions for the CLI
+ const view_functions = [
+ "get_admin",
+ "get_flash_fee",
+ "get_protocol_fee",
+ "get_protocol_revenues",
+ ];
+ const functions = [
+ "set_flash_fee",
+ "set_protocol_fee",
+ "claim_protocol_revenues",
+ "quit",
+ ];
+
+ console.log("\nAvailable view functions:");
+ view_functions.forEach((func, index) =>
+ console.log("๐งช " + `${index + 1}. ${func}`)
+ );
+
+ console.log(
+ "\nAvailable Write (last transaction hash: " + lastTransactionHash + ")"
+ );
+ functions.forEach((func, index) =>
+ console.log("๐งช " + `${index + 1}. ${func}`)
+ );
+
+ const functionName = await ask(
+ "\nWhich function would you like to execute? (Enter the name): "
+ );
+
+ if (functionName.trim() === "quit") {
+ console.log("Exiting program.");
+ break;
+ }
+
+ switch (functionName.trim()) {
+ case "get_protocol_fee":
+ // Collect parameters for create_with_duration
+ let token_ = await ask("Enter the asset address: ");
+ // Call the function with collected parameters
+ await get_protocol_fee(token_);
+ break;
+ // Similar structure for other functions
+ case "get_flash_fee":
+ // Collect parameters and call create_with_range
+ await get_flash_fee();
+ break;
+ case "get_admin":
+ // Call the function with collected parameters
+ await get_admin();
+ break;
+ case "claim_protocol_revenues":
+ // Collect parameters for cancel_multiple
+ const asset = await ask("Enter the asset : ");
+
+ // Call the function with collected parameters
+ await claim_protocol_revenues(asset);
+ break;
+ case "set_flash_fee":
+ // Collect parameters for withdraw_multiple
+
+ const flash_fee = parseInt(await ask("Enter the new flash fee: "));
+ // Call the function with collected parameters
+ await set_flash_fee(flash_fee);
+ break;
+ case "set_protocol_fee":
+ const protocol_fee = parseInt(
+ await ask("Enter the new protocol fee: ")
+ );
+ await set_protocol_fee(protocol_fee);
+ break;
+ case "get_protocol_revenues":
+ // Collect parameters and call create_with_range
+ await get_protocol_revenues();
+ break;
+
+ default:
+ console.log("Function not recognized.");
+ break;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 5000));
+ }
+
+ rl.close();
+}
+
+export async function initialize_account() {
+ const provider = new RpcProvider({
+ nodeUrl: "SN_GOERLI",
+ });
+
+ // Check that communication with provider is OK
+ const ci = await provider.getChainId();
+ console.log("chain Id =", ci);
+
+ // initialize existing Argent X testnet account
+ const adminAccountAddress =
+ "0x05D20A56d451F02B50486B7d7B2b3F25F5A594Da8AA620Ca599fd65E7312b7F4";
+ const adminPrivateKey =
+ "0x06ab1f177bbf6b9d862412f0ec4feb0bdc520c7712f5a25c3e043cbaa29410db";
+
+ const recipientAccountAddress =
+ "0x1ad3cd865329587101b3a2c3e0b7c9ca8ac9d538f6c2179384108d8ff7e6b3d";
+ const recipientPrivateKey =
+ "0x05a2fe8a27eb75fb978d4ef568dbde3a0e72f0caffd30096db070d5ddba23f2a";
+
+ const recipientAccount = new Account(
+ provider,
+ recipientAccountAddress,
+ recipientPrivateKey
+ );
+
+ // // initialize existing Argent X mainnet account
+ // const privateKey = account4MainnetPrivateKey;
+ // const accountAddress = account4MainnetAddress
+ const account0 = new Account(provider, adminAccountAddress, adminPrivateKey);
+ console.log("existing_ACCOUNT_ADDRESS=", adminAccountAddress);
+ console.log("existing account connected.\n");
+
+ const erc20CompiledSierra = json.parse(
+ fs
+ .readFileSync("target/dev/tokei_ERC20.contract_class.json")
+ .toString("ascii")
+ );
+ const erc20CompiledCasm = json.parse(
+ fs
+ .readFileSync("target/dev/tokei_ERC20.compiled_contract_class.json")
+ .toString("ascii")
+ );
+
+ const tokeiCompiledSierra = json.parse(
+ fs
+ .readFileSync("target/dev/tokei_TokeiLockupLinear.contract_class.json")
+ .toString("ascii")
+ );
+ const compiledCasm = json.parse(
+ fs
+ .readFileSync(
+ "target/dev/tokei_TokeiLockupLinear.compiled_contract_class.json"
+ )
+ .toString("ascii")
+ );
+
+ const erc20ClassHash =
+ "0x01645152801d7bef56b3bdc02e0f13bbfb5646f3b3bda875f633df8f9b58b35d";
+ const erc20address =
+ "0x075b1b684be1cd0f08a4a59a22994dedb6d3f5851e630b3f1a895459ef754e87";
+
+ const tokeiClassHash =
+ "0x01f9313b620810859fb1aa2b6920bb80a00d6bf5b13d1329b9a82424c8c272ab";
+ const tokeiaddress =
+ "0x04bf83b5554b165b5f0ff5e797a8f57162840c78915b4864bdbfbdc71649ef1b";
+
+ console.log("โ
ERC20 Contract declared with classHash =", erc20ClassHash);
+ console.log("โ
Tokei Contract declared with classHash =", tokeiClassHash);
+
+ // *************************************************************************
+ // CONTRACT CONNECTION
+ // *************************************************************************
+
+ const tokeiContract = new Contract(
+ tokeiCompiledSierra.abi,
+ tokeiaddress,
+ provider
+ );
+ console.log("โ
Tokei Contract connected at =", tokeiContract.address);
+ tokeiContract.connect(account0);
+
+ const erc20Contract = new Contract(
+ erc20CompiledSierra.abi,
+ erc20address,
+ provider
+ );
+ console.log("โ
ERC20 Contract connected at =", erc20Contract.address);
+ erc20Contract.connect(account0);
+
+ return { account0, recipientAccount, tokeiContract, erc20Contract, provider };
+}
+async function get_protocol_fee(asset: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await tokeiContract.call(
+ "get_protocol_fee",
+ CallData.compile({ asset: asset })
+ );
+
+ console.log("protocol fee =", success.toString());
+}
+
+async function get_protocol_revenues() {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await tokeiContract.call(
+ "get_protocol_revenues",
+ CallData.compile({ asset: erc20Contract.address })
+ );
+
+ console.log("โ
protocol revenues for the given asset :", success.toString());
+}
+
+async function set_protocol_fee(protocol_fee: number) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await account0.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "set_protocol_fee",
+ calldata: CallData.compile({
+ asset: erc20Contract.address,
+ amount: cairo.uint256(protocol_fee),
+ }),
+ },
+ ]);
+
+ console.log(
+ "โ
protocol fee has been set -> Transaction Hash :",
+ success.transaction_hash
+ );
+}
+
+async function set_flash_fee(flash_fee: number) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await account0.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "set_flash_fee",
+ calldata: CallData.compile({
+ amount: cairo.uint256(flash_fee),
+ }),
+ },
+ ]);
+
+ console.log(
+ "โ
flash fee has been set -> Transaction Hash :",
+ success.transaction_hash
+ );
+}
+
+async function get_flash_fee() {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await tokeiContract.call("get_flash_fee");
+
+ console.log("โ
flash fee =", success.toString());
+}
+async function claim_protocol_revenues(asset: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success2 = await account0.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "claim_protocol_revenues",
+ calldata: CallData.compile({
+ asset: asset,
+ }),
+ },
+ ]);
+
+ console.log(
+ "โ
protocol fee has been claimed -> Transaction Hash :",
+ success2.transaction_hash
+ );
+}
+
+async function get_admin() {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success2 = await tokeiContract.call("get_admin");
+ let res = success2.valueOf();
+
+ console.log("โ
The protocol admin is -> :", "0x" + res.toString(16));
+}
+
+main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
diff --git a/src/scripts/deployment.ts b/src/scripts/deployment.ts
new file mode 100644
index 0000000..258c79a
--- /dev/null
+++ b/src/scripts/deployment.ts
@@ -0,0 +1,93 @@
+import {
+ constants,
+ Provider,
+ Contract,
+ Account,
+ json,
+ shortString,
+ RpcProvider,
+ hash,
+} from "starknet";
+import fs from "fs";
+
+async function main() {
+ // Initialize RPC provider with a specified node URL (Goerli testnet in this case)
+ const provider = new RpcProvider({
+ nodeUrl: "SN_GOERLI",
+ });
+
+ // Check that communication with provider is OK
+ const ci = await provider.getChainId();
+ console.log("chain Id =", ci);
+
+ // initialize existing Argent X testnet account
+ const accountAddress =
+ "0x05D20A56d451F02B50486B7d7B2b3F25F5A594Da8AA620Ca599fd65E7312b7F4";
+ const privateKey =
+ "0x06ab1f177bbf6b9d862412f0ec4feb0bdc520c7712f5a25c3e043cbaa29410db";
+
+ const account0 = new Account(provider, accountAddress, privateKey);
+ console.log("existing_ACCOUNT_ADDRESS=", accountAddress);
+ console.log("existing account connected.\n");
+
+ // Parse the compiled contract files
+ const compiledSierra = json.parse(
+ fs
+ .readFileSync("target/dev/tokei_TokeiLockupLinear.contract_class.json")
+ .toString("ascii")
+ );
+ const compiledCasm = json.parse(
+ fs
+ .readFileSync(
+ "target/dev/tokei_TokeiLockupLinear.compiled_contract_class.json"
+ )
+ .toString("ascii")
+ );
+
+ //**************************************************************************************** */
+ // Since we already have the classhash we will be skipping this part
+ // Declare the contract
+
+ // const ch = hash.computeSierraContractClassHash(compiledSierra);
+ // console.log("Class hash calc =", ch);
+ // const compCH = hash.computeCompiledClassHash(compiledCasm);
+ // console.log("compiled class hash =", compCH);
+ // const declareResponse = await account0.declare({
+ // contract: compiledSierra,
+ // casm: compiledCasm,
+ // });
+ // const contractClassHash = declareResponse.class_hash;
+
+ // // Wait for the transaction to be confirmed and log the transaction receipt
+ // const txR = await provider.waitForTransaction(
+ // declareResponse.transaction_hash
+ // );
+ // console.log("tx receipt =", txR);
+
+ //**************************************************************************************** */
+
+ const contractClassHash =
+ "0x01f9313b620810859fb1aa2b6920bb80a00d6bf5b13d1329b9a82424c8c272ab";
+
+ console.log("โ
Test Contract declared with classHash =", contractClassHash);
+
+ console.log("Deploy of contract in progress...");
+ const { transaction_hash: th2, address } = await account0.deployContract({
+ classHash: contractClassHash,
+ constructorCalldata: [accountAddress],
+ });
+ console.log("๐ contract_address =", address);
+ // Wait for the deployment transaction to be confirmed
+ await provider.waitForTransaction(th2);
+
+ console.log("โ
Test completed.");
+}
+main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
+
+//Deployed Address
+// Updated deployed contract_address : 0x04bf83b5554b165b5f0ff5e797a8f57162840c78915b4864bdbfbdc71649ef1b on goerli
diff --git a/src/scripts/user_example.txt b/src/scripts/user_example.txt
new file mode 100644
index 0000000..f6ad02b
--- /dev/null
+++ b/src/scripts/user_example.txt
@@ -0,0 +1,17 @@
+let sender =
+ "0x05D20A56d451F02B50486B7d7B2b3F25F5A594Da8AA620Ca599fd65E7312b7F4";
+ let recipient =
+ "0x01ad3cD865329587101B3a2c3e0B7C9ca8ac9D538F6c2179384108d8ff7E6B3d";
+ let total_amount = cairo.uint256(12000000000000000000);
+ let asset =
+ "0x075b1b684be1cd0f08a4a59a22994dedb6d3f5851e630b3f1a895459ef754e87";
+ let cancelable = true;
+ let transferable = true;
+ let duration_cliff = 430; // 430/60 = 7.16 minutes
+ let duration_total = 700; // 700/60 = 11.66 minutes
+ let broker_account =
+ "0x0375b883a5A4624660EF419ed58a3c7C3ba262100CA6eE7056B65d7EB745F933";
+ let broker_fee = cairo.uint256(3); // 0.03%
+ let range_start = 1706132876;
+ let range_cliff = 1706133071;
+ let range_end = 1706139471;
\ No newline at end of file
diff --git a/src/scripts/user_interaction.ts b/src/scripts/user_interaction.ts
new file mode 100644
index 0000000..1ed1f7e
--- /dev/null
+++ b/src/scripts/user_interaction.ts
@@ -0,0 +1,1032 @@
+import {
+ constants,
+ Provider,
+ Contract,
+ Account,
+ json,
+ shortString,
+ RpcProvider,
+ hash,
+ CallData,
+ cairo,
+ Uint256,
+} from "starknet";
+import fs from "fs";
+import readline from "readline";
+import dotenv from "dotenv";
+import { utils } from "@project-serum/anchor";
+dotenv.config();
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+});
+
+function ask(question: string): Promise {
+ return new Promise((resolve) => rl.question(question, resolve));
+}
+
+async function main() {
+ // // *************************************************************************
+ // // TEST VALUES
+ // // *************************************************************************
+
+ // Please feel free to change the values of the variables below to test out the flow
+ // The values below are just an example of how to use the functions
+ // You can mint yourself some tokens from this address 0x075b1b684be1cd0f08a4a59a22994dedb6d3f5851e630b3f1a895459ef754e87
+ let sender =
+ "0x05D20A56d451F02B50486B7d7B2b3F25F5A594Da8AA620Ca599fd65E7312b7F4";
+ let recipient =
+ "0x01ad3cD865329587101B3a2c3e0B7C9ca8ac9D538F6c2179384108d8ff7E6B3d";
+ let total_amount = cairo.uint256(12000000000000000000);
+ let asset =
+ "0x075b1b684be1cd0f08a4a59a22994dedb6d3f5851e630b3f1a895459ef754e87";
+ let cancelable = true;
+ let transferable = true;
+ let duration_cliff = 430; // 430/60 = 7.16 minutes
+ let duration_total = 700; // 700/60 = 11.66 minutes
+ let broker_account =
+ "0x0375b883a5A4624660EF419ed58a3c7C3ba262100CA6eE7056B65d7EB745F933";
+ let broker_fee = cairo.uint256(3); // 0.03%
+ let range_start = 1706132876;
+ let range_cliff = 1706133071;
+ let range_end = 1706139471;
+
+ let lastTransactionHash = "";
+ while (true) {
+ // List of functions for the CLI
+ const view_functions = [
+ "get_asset",
+ "get_protocol_fee",
+ "get_protocol_revenues",
+ "get_cliff_time",
+ "get_deposited_amount",
+ "get_end_time",
+ "get_range",
+ "get_refunded_amount",
+ "get_sender",
+ "get_start_time",
+ "get_stream",
+ "get_withdrawn_amount",
+ "is_cancelable",
+ "is_transferable",
+ "is_depleted",
+ "is_stream",
+ "refundable_amount_of",
+ "get_recipient",
+ "is_cold",
+ "is_warm",
+ "withdrawable_amount_of",
+ "status_of",
+ "streamed_amount_of",
+ "was_canceled",
+ "get_streams_by_sender",
+ "get_streams_by_recipient",
+ "get_streams_ids_by_sender",
+ "get_streams_ids_by_recipient",
+ ];
+ const functions = [
+ "create_with_duration",
+ "create_with_range",
+ "cancel_stream",
+ "cancel_multiple",
+ "withdraw_max",
+ "withdraw_multiple",
+ "withdraw_max_and_transfer",
+ "quit",
+ ];
+
+ console.log("\nAvailable view functions:");
+ view_functions.forEach((func, index) =>
+ console.log("๐งช " + `${index + 1}. ${func}`)
+ );
+
+ console.log(
+ "\nAvailable Write (last transaction hash: " + lastTransactionHash + ")"
+ );
+ functions.forEach((func, index) =>
+ console.log("๐งช " + `${index + 1}. ${func}`)
+ );
+
+ const functionName = await ask(
+ "\nWhich function would you like to execute? (Enter the name): "
+ );
+
+ if (functionName.trim() === "quit") {
+ console.log("Exiting program.");
+ break;
+ }
+
+ switch (functionName.trim()) {
+ case "create_with_duration":
+ // Collect parameters for create_with_duration
+ const sender = await ask("Enter sender: ");
+ const recipient = await ask("Enter recipient: ");
+ const total_amount = cairo.uint256(await ask("Enter total amount: "));
+ const asset = await ask("Enter asset: ");
+ const cancelable =
+ (await ask("Is it cancelable? (true/false): ")) === "true";
+ const transferable =
+ (await ask("Is it transferable? (true/false): ")) === "true";
+ const duration_cliff = parseInt(await ask("Enter duration cliff: "));
+ const duration_total = parseInt(await ask("Enter duration total: "));
+ const broker_account = await ask("Enter broker account: ");
+ const broker_fee = cairo.uint256(await ask("Enter broker fee: "));
+ await create_with_duration(
+ sender,
+ recipient,
+ total_amount,
+ asset,
+ cancelable,
+ transferable,
+ duration_cliff,
+ duration_total,
+ broker_account,
+ broker_fee
+ );
+ break;
+ // Similar structure for other functions
+ case "create_with_range":
+ // Collect parameters and call create_with_range
+ const sender_3 = await ask("Enter sender: ");
+ const recipient_3 = await ask("Enter recipient: ");
+ const total_amount_3 = cairo.uint256(await ask("Enter total amount: "));
+ const asset_3 = await ask("Enter asset: ");
+ const cancelable_3 =
+ (await ask("Is it cancelable? (true/false): ")) === "true";
+ const transferable_3 =
+ (await ask("Is it transferable? (true/false): ")) === "true";
+ const range_start_3 = parseInt(await ask("Enter range start: "));
+ const range_cliff_3 = parseInt(await ask("Enter range cliff: "));
+ const range_end_3 = parseInt(await ask("Enter range end: "));
+ const broker_account_3 = await ask("Enter broker account: ");
+ const broker_fee_3 = cairo.uint256(await ask("Enter broker fee: "));
+ await create_with_range(
+ sender_3,
+ recipient_3,
+ total_amount_3,
+ asset_3,
+ cancelable_3,
+ transferable_3,
+ range_start_3,
+ range_cliff_3,
+ range_end_3,
+ broker_account_3,
+ broker_fee_3
+ );
+
+ break;
+ case "cancel_stream":
+ // Collect parameters for cancel_stream
+ const streamId = await ask("Enter stream ID: ");
+ // Call the function with collected parameters
+ await cancel_stream(streamId);
+ break;
+ case "cancel_multiple":
+ // Collect parameters for cancel_multiple
+ const streamIds = (await ask("Enter stream IDs (comma-separated): "))
+ .split(",")
+ .map((id) => parseInt(id.trim()));
+ // Call the function with collected parameters
+ await cancel_multiple(streamIds);
+ break;
+ case "withdraw_max":
+ // Collect parameters for withdraw_max
+ const streamIdForWithdraw = await ask("Enter stream ID: ");
+ let recipientAddress = await ask("Enter recipient address: ");
+ // Call the function with collected parameters
+ await withdraw_max(streamIdForWithdraw, recipientAddress);
+ break;
+ case "withdraw_multiple":
+ // Collect parameters for withdraw_multiple
+ const streamIdsForWithdraw = (
+ await ask("Enter stream IDs for withdrawal (comma-separated): ")
+ )
+ .split(",")
+ .map((id) => parseInt(id.trim()));
+ const amounts = (await ask("Enter amounts (comma-separated): "))
+ .split(",")
+ .map((amount) => parseInt(amount.trim()));
+ // Call the function with collected parameters
+ await withdraw_multiple(streamIdsForWithdraw, amounts);
+ break;
+ case "withdraw_max_and_transfer":
+ // Collect parameters for withdraw_max_and_transfer
+ const streamIdForMaxTransfer = await ask("Enter stream ID: ");
+ const transferRecipient = await ask("Enter recipient: ");
+ // Call the function with collected parameters
+ await withdraw_max_and_transfer(
+ streamIdForMaxTransfer,
+ transferRecipient
+ );
+ break;
+ case "get_asset":
+ const streamIdForAsset = await ask("Enter stream ID: ");
+ await get_asset(streamIdForAsset);
+ break;
+ case "get_protocol_fee":
+ let stream_id = await ask("Enter stream ID: ");
+ await get_protocol_fee(stream_id);
+ break;
+ case "get_protocol_revenues":
+ let asset2 = await ask("Enter asset: ");
+ await get_protocol_revenues(asset2);
+ break;
+ case "get_cliff_time":
+ let stream_id2 = await ask("Enter stream ID: ");
+ await get_cliff_time(stream_id2);
+ break;
+ case "get_deposited_amount":
+ let stream_id3 = await ask("Enter stream ID: ");
+ await get_deposited_amount(stream_id3);
+ break;
+ case "get_end_time":
+ let stream_id4 = await ask("Enter stream ID: ");
+ await get_end_time(stream_id4);
+ break;
+ case "get_range":
+ let stream_id5 = await ask("Enter stream ID: ");
+ await get_range(stream_id5);
+ break;
+ case "get_refunded_amount":
+ let stream_id6 = await ask("Enter stream ID: ");
+ await get_refunded_amount(stream_id6);
+ break;
+ case "get_sender":
+ let stream_id7 = await ask("Enter stream ID: ");
+ await get_sender(stream_id7);
+ break;
+ case "get_start_time":
+ let stream_id8 = await ask("Enter stream ID: ");
+ await get_start_time(stream_id8);
+ break;
+ case "get_withdrawn_amount":
+ let stream_id10 = await ask("Enter stream ID: ");
+ await get_withdrawn_amount(stream_id10);
+ break;
+ case "is_cancelable":
+ let stream_id11 = await ask("Enter stream ID: ");
+ await is_cancelable(stream_id11);
+ break;
+ case "is_transferable":
+ let stream_id12 = await ask("Enter stream ID: ");
+ await is_transferable(stream_id12);
+ break;
+ case "is_depleted":
+ let stream_id13 = await ask("Enter stream ID: ");
+ await is_depleted(stream_id13);
+ break;
+ case "is_stream":
+ let stream_id14 = await ask("Enter stream ID: ");
+ await is_stream(stream_id14);
+ break;
+ case "refundable_amount_of":
+ let stream_id15 = await ask("Enter stream ID: ");
+ await refundable_amount_of(stream_id15);
+ break;
+ case "get_recipient":
+ let stream_id16 = await ask("Enter stream ID: ");
+ await get_recipient(stream_id16);
+ break;
+ case "is_cold":
+ let stream_id17 = await ask("Enter stream ID: ");
+ let account2 = await ask("Enter account: ");
+ await is_cold(stream_id17, account2);
+ break;
+ case "is_warm":
+ let stream_id18 = await ask("Enter stream ID: ");
+ let account3 = await ask("Enter account: ");
+ await is_warm(stream_id18, account3);
+ break;
+ case "withdrawable_amount_of":
+ let stream_id19 = await ask("Enter stream ID: ");
+ let account4 = await ask("Enter account: ");
+ await withdrawable_amount_of(stream_id19, account4);
+ break;
+ case "status_of":
+ let stream_id20 = await ask("Enter stream ID: ");
+ let account5 = await ask("Enter account: ");
+ await status_of(stream_id20, account5);
+ break;
+ case "streamed_amount_of":
+ let stream_id21 = await ask("Enter stream ID: ");
+
+ await streamed_amount_of(stream_id21);
+ break;
+ case "was_canceled":
+ let stream_id22 = await ask("Enter stream ID: ");
+ await was_canceled(stream_id22);
+ break;
+ case "get_streams_by_sender":
+ let sender2 = await ask("Enter sender: ");
+ await get_streams_by_sender(sender2);
+ break;
+ case "get_streams_by_recipient":
+ let recipient2 = await ask("Enter recipient: ");
+ await get_streams_by_recipient(recipient2);
+ break;
+ case "get_streams_ids_by_sender":
+ let sender3 = await ask("Enter sender: ");
+ await get_streams_ids_by_sender(sender3);
+ break;
+ case "get_streams_ids_by_recipient":
+ let recipient3 = await ask("Enter recipient: ");
+ await get_streams_ids_by_recipient(recipient3);
+ break;
+
+ default:
+ console.log("Function not recognized.");
+ break;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 5000));
+ }
+
+ rl.close();
+}
+
+main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ rl.close();
+ process.exit(1);
+ });
+
+export async function initialize_account() {
+ const provider = new RpcProvider({
+ nodeUrl: "SN_GOERLI",
+ });
+
+ // Check that communication with provider is OK
+ const ci = await provider.getChainId();
+ console.log("chain Id =", ci);
+
+ // initialize existing Argent X testnet account
+ const adminAccountAddress =
+ "0x05D20A56d451F02B50486B7d7B2b3F25F5A594Da8AA620Ca599fd65E7312b7F4";
+ const adminPrivateKey =
+ "0x06ab1f177bbf6b9d862412f0ec4feb0bdc520c7712f5a25c3e043cbaa29410db";
+
+ const recipientAccountAddress =
+ "0x1ad3cd865329587101b3a2c3e0b7c9ca8ac9d538f6c2179384108d8ff7e6b3d";
+ const recipientPrivateKey =
+ "0x05a2fe8a27eb75fb978d4ef568dbde3a0e72f0caffd30096db070d5ddba23f2a";
+
+ const recipientAccount = new Account(
+ provider,
+ recipientAccountAddress,
+ recipientPrivateKey
+ );
+
+ // // initialize existing Argent X mainnet account
+ // const privateKey = account4MainnetPrivateKey;
+ // const accountAddress = account4MainnetAddress
+ const account0 = new Account(provider, adminAccountAddress, adminPrivateKey);
+ console.log("existing_ACCOUNT_ADDRESS=", adminAccountAddress);
+ console.log("existing account connected.\n");
+
+ const erc20CompiledSierra = json.parse(
+ fs
+ .readFileSync("target/dev/tokei_ERC20.contract_class.json")
+ .toString("ascii")
+ );
+ const erc20CompiledCasm = json.parse(
+ fs
+ .readFileSync("target/dev/tokei_ERC20.compiled_contract_class.json")
+ .toString("ascii")
+ );
+
+ const tokeiCompiledSierra = json.parse(
+ fs
+ .readFileSync("target/dev/tokei_TokeiLockupLinear.contract_class.json")
+ .toString("ascii")
+ );
+ const compiledCasm = json.parse(
+ fs
+ .readFileSync(
+ "target/dev/tokei_TokeiLockupLinear.compiled_contract_class.json"
+ )
+ .toString("ascii")
+ );
+
+ const erc20ClassHash =
+ "0x01645152801d7bef56b3bdc02e0f13bbfb5646f3b3bda875f633df8f9b58b35d";
+ const erc20address =
+ "0x075b1b684be1cd0f08a4a59a22994dedb6d3f5851e630b3f1a895459ef754e87";
+
+ const tokeiClassHash =
+ "0x01f9313b620810859fb1aa2b6920bb80a00d6bf5b13d1329b9a82424c8c272ab";
+ const tokeiaddress =
+ "0x04bf83b5554b165b5f0ff5e797a8f57162840c78915b4864bdbfbdc71649ef1b";
+
+ // console.log("โ
ERC20 Contract declared with classHash =", erc20ClassHash);
+ // console.log("โ
Tokei Contract declared with classHash =", tokeiClassHash);
+
+ // *************************************************************************
+ // CONTRACT CONNECTION
+ // *************************************************************************
+
+ const tokeiContract = new Contract(
+ tokeiCompiledSierra.abi,
+ tokeiaddress,
+ provider
+ );
+ // console.log("โ
Tokei Contract connected at =", tokeiContract.address);
+ tokeiContract.connect(account0);
+
+ const erc20Contract = new Contract(
+ erc20CompiledSierra.abi,
+ erc20address,
+ provider
+ );
+ // console.log("โ
ERC20 Contract connected at =", erc20Contract.address);
+ erc20Contract.connect(account0);
+
+ console.log(" ๐ค In process ...");
+
+ return { account0, recipientAccount, tokeiContract, erc20Contract, provider };
+}
+
+export async function cancel_stream(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par6 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success6 = await account0.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "cancel",
+ calldata: par6,
+ },
+ ]);
+
+ console.log("โ
cancel_stream invoked at :", success6.transaction_hash);
+}
+
+async function withdraw_max(stream_id: string, recipientAddress: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par3 = CallData.compile({
+ stream_id: stream_id,
+ to: recipientAddress,
+ });
+
+ let success3 = await recipientAccount.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "withdraw_max",
+ calldata: par3,
+ },
+ ]);
+
+ console.log("โ
withdraw_max invoked at :", success3.transaction_hash);
+}
+
+async function withdraw_multiple(stream_ids: number[], amounts: number[]) {
+ let amounts_mod = amounts.map((amount) => cairo.uint256(amount));
+
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par4 = CallData.compile({
+ stream_ids: stream_ids,
+ to: recipientAccount.address,
+ amounts: amounts_mod,
+ });
+
+ let success4 = await account0.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "withdraw_multiple",
+ calldata: par4,
+ },
+ ]);
+
+ console.log("โ
withdraw_multiple invoked at :", success4.transaction_hash);
+}
+
+async function withdraw_max_and_transfer(stream_id: string, to: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par5 = CallData.compile({
+ stream_id: stream_id,
+ to: to,
+ });
+
+ let success5 = await recipientAccount.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "withdraw_max_and_transfer",
+ calldata: par5,
+ },
+ ]);
+
+ console.log(
+ "โ
withdraw_max_and_transfer invoked at :",
+ success5.transaction_hash
+ );
+}
+
+async function create_with_duration(
+ sender: string,
+ recipient: string,
+ total_amount: Uint256,
+ asset: string,
+ cancelable: boolean,
+ transferable: boolean,
+ duration_cliff: number,
+ duration_total: number,
+ broker_account: string,
+ broker_fee: Uint256
+) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ // Calldata for Create_with_duration
+ const par1 = CallData.compile({
+ sender: sender,
+ recipient: recipient,
+ total_amount: total_amount,
+ asset: asset,
+ cancelable: cancelable,
+ transferable: transferable,
+ duration_cliff: duration_cliff,
+ duration_total: duration_total,
+ broker_account: broker_account,
+ broker_fee: broker_fee,
+ });
+
+ // // *************************************************************************************
+ // // TOKEN APPROVAL TO THE TOKEI CONTRACT & CREATE_WITH_DURATION
+ // // ****************************************************************************************
+ // // Multicall transaction with approval and create_with_duration
+ let success = await account0.execute([
+ {
+ contractAddress: erc20Contract.address,
+ entrypoint: "approve",
+ calldata: CallData.compile({
+ recipient: tokeiContract.address,
+ amount: total_amount,
+ }),
+ },
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "create_with_duration",
+ calldata: par1,
+ },
+ ]);
+ console.log("โ
create_with_duration invoked at :", success.transaction_hash);
+}
+
+async function create_with_range(
+ sender: string,
+ recipient: string,
+ total_amount: Uint256,
+ asset: string,
+ cancelable: boolean,
+ transferable: boolean,
+ range_start: number,
+ range_cliff: number,
+ range_end: number,
+ broker_account: string,
+ broker_fee: Uint256
+) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ // Calldata for Create_with_range
+
+ let par2 = CallData.compile({
+ sender: sender,
+ recipient: recipient,
+ total_amount: total_amount,
+ asset: asset,
+ cancelable: cancelable,
+ transferable: transferable,
+ range_start: range_start,
+ range_cliff: range_cliff,
+ range_end: range_end,
+ broker_account: broker_account,
+ broker_fee: broker_fee,
+ });
+
+ // Multicall transaction with approval and create_with_range
+ let success2 = await account0.execute([
+ {
+ contractAddress: erc20Contract.address,
+ entrypoint: "approve",
+ calldata: CallData.compile({
+ recipient: tokeiContract.address,
+ amount: total_amount,
+ }),
+ },
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "create_with_range",
+ calldata: par2,
+ },
+ ]);
+
+ console.log("โ
create_with_range invoked at :", success2.transaction_hash);
+}
+
+async function cancel_multiple(stream_ids: number[]) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ // Calldata for Cancel_multiple
+ let par7 = CallData.compile({
+ stream_ids: stream_ids,
+ });
+
+ let success7 = await account0.execute([
+ {
+ contractAddress: tokeiContract.address,
+ entrypoint: "cancel_multiple",
+ calldata: par7,
+ },
+ ]);
+
+ console.log("โ
cancel_multiple invoked at :", success7.transaction_hash);
+}
+
+async function get_asset(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await tokeiContract.call(
+ "get_asset",
+ CallData.compile({
+ stream_id: stream_id,
+ })
+ );
+
+ console.log("โ
asset =", success.toString());
+}
+
+async function get_protocol_fee(asset: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await tokeiContract.call(
+ "get_protocol_fee",
+ CallData.compile({ asset: asset })
+ );
+
+ console.log("โ
protocol fee =", success.toString());
+}
+
+async function get_protocol_revenues(asset: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let success = await tokeiContract.call(
+ "get_protocol_revenues",
+ CallData.compile({ asset: asset })
+ );
+
+ console.log("โ
protocol revenues for the given asset :", success.toString());
+}
+
+async function get_cliff_time(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par1 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_cliff_time", par1);
+
+ console.log("โ
cliff time =", success.toString());
+}
+
+async function get_deposited_amount(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par2 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_deposited_amount", par2);
+
+ console.log("โ
deposited amount =", success.toString());
+}
+
+async function get_end_time(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par3 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_end_time", par3);
+
+ console.log("โ
end time =", success.toString());
+}
+
+async function get_range(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par4 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_range", par4);
+
+ console.log("โ
range =", success.valueOf());
+}
+
+async function get_refunded_amount(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par5 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_refunded_amount", par5);
+
+ console.log("โ
refunded amount =", success.toString());
+}
+
+async function get_sender(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par6 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_sender", par6);
+
+ console.log("โ
sender =", success.toString());
+}
+
+async function get_start_time(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par7 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_start_time", par7);
+
+ console.log("โ
start time =", success.toString());
+}
+
+async function get_streams_by_sender(sender: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par8 = CallData.compile({
+ sender: sender,
+ });
+
+ let success = await tokeiContract.call("get_streams_by_sender", par8);
+
+ console.log("โ
streams =", success.valueOf());
+}
+
+async function get_streams_by_recipient(recipient: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par9 = CallData.compile({
+ recipient: recipient,
+ });
+
+ let success = await tokeiContract.call("get_streams_by_recipient", par9);
+
+ console.log("โ
streams =", success.valueOf());
+}
+
+async function get_streams_ids_by_sender(sender: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par10 = CallData.compile({
+ sender: sender,
+ });
+
+ let success = await tokeiContract.call("get_streams_ids_by_sender", par10);
+
+ console.log("โ
streams =", success.toString());
+}
+
+async function get_streams_ids_by_recipient(recipient: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par11 = CallData.compile({
+ recipient: recipient,
+ });
+
+ let success = await tokeiContract.call("get_streams_ids_by_recipient", par11);
+
+ console.log("โ
streams =", success.toString());
+}
+
+async function get_withdrawn_amount(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par9 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_withdrawn_amount", par9);
+
+ console.log("โ
withdrawn amount =", success.toString());
+}
+
+async function is_cancelable(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par10 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("is_cancelable", par10);
+
+ console.log("โ
cancelable =", success.toString());
+}
+
+async function is_transferable(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par11 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("is_transferable", par11);
+
+ console.log("โ
transferable =", success.toString());
+}
+
+async function is_depleted(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par12 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("is_depleted", par12);
+
+ console.log("โ
depleted =", success.toString());
+}
+
+async function is_stream(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+ let par13 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("is_stream", par13);
+
+ console.log("โ
stream =", success.toString());
+}
+
+async function refundable_amount_of(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par14 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("refundable_amount_of", par14);
+
+ console.log("โ
refundable amount =", success.toString());
+}
+
+async function get_recipient(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par15 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("get_recipient", par15);
+
+ console.log("โ
recipient =", success.toString());
+}
+
+async function is_cold(stream_id: string, account: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par16 = CallData.compile({
+ stream_id: stream_id,
+ account: account,
+ });
+
+ let success = await tokeiContract.call("is_cold", par16);
+
+ console.log("โ
cold =", success.toString());
+}
+
+async function is_warm(stream_id: string, account: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par17 = CallData.compile({
+ stream_id: stream_id,
+ account: account,
+ });
+
+ let success = await tokeiContract.call("is_warm", par17);
+
+ console.log("โ
warm =", success.toString());
+}
+
+async function withdrawable_amount_of(stream_id: string, account: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par18 = CallData.compile({
+ stream_id: stream_id,
+ account: account,
+ });
+
+ let success = await tokeiContract.call("withdrawable_amount_of", par18);
+
+ console.log("โ
withdrawable amount =", success.toString());
+}
+
+async function status_of(stream_id: string, account: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par19 = CallData.compile({
+ stream_id: stream_id,
+ account: account,
+ });
+
+ let success = await tokeiContract.call("status_of", par19);
+
+ console.log("โ
status =", success.toString());
+}
+
+async function streamed_amount_of(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par20 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("streamed_amount_of", par20);
+
+ console.log("โ
streamed amount =", success.toString());
+}
+
+async function was_canceled(stream_id: string) {
+ const info = await initialize_account();
+ const { account0, recipientAccount, tokeiContract, erc20Contract, provider } =
+ info;
+
+ let par21 = CallData.compile({
+ stream_id: stream_id,
+ });
+
+ let success = await tokeiContract.call("was_canceled", par21);
+
+ console.log("โ
canceled =", success.toString());
+}
+
+main()
+ .then(() => process.exit(0))
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
diff --git a/src/tokens/erc20.cairo b/src/tests/mocks/erc20.cairo
similarity index 100%
rename from src/tokens/erc20.cairo
rename to src/tests/mocks/erc20.cairo
diff --git a/src/tests/test_lockup_linear.cairo b/src/tests/test_lockup_linear.cairo
new file mode 100644
index 0000000..0067051
--- /dev/null
+++ b/src/tests/test_lockup_linear.cairo
@@ -0,0 +1,1248 @@
+//! Test file for `src/core/lockup_linear.cairo`.
+
+// *************************************************************************
+// IMPORTS
+// *************************************************************************
+
+// Core lib imports.
+use array::ArrayTrait;
+use result::ResultTrait;
+use option::OptionTrait;
+use traits::{TryInto, Into};
+use starknet::{
+ ContractAddress, get_caller_address, Felt252TryIntoContractAddress, contract_address_const,
+ ClassHash,
+};
+use debug::PrintTrait;
+use integer::BoundedInt;
+
+// Starknet Foundry imports.
+use snforge_std::{
+ declare, ContractClassTrait, start_prank, stop_prank, RevertedTransaction, CheatTarget,
+ TxInfoMock, start_warp, stop_warp
+};
+
+use tokei::tests::utils::utils::Utils::{
+ pow_256, ADMIN, ASSET, ALICE, CHARLIE, setup, teardown, prepare_contracts, deploy_setup_erc20,
+ deploy_tokei, give_tokens_and_approve, BOB,
+};
+use tokei::tests::utils::defaults::Defaults::{PROTOCOL_FEES, RECIPIENT, BROKER};
+use tokei::tests::utils::defaults::Defaults;
+
+// use tokei::tokens::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait};
+use openzeppelin::token::erc20::interface::{
+ IERC20, IERC20Metadata, ERC20ABIDispatcher, ERC20ABIDispatcherTrait
+};
+
+// Local imports.
+use tokei::core::lockup_linear::{ITokeiLockupLinearDispatcher, ITokeiLockupLinearDispatcherTrait};
+use tokei::types::lockup_linear::{Range, Broker, Durations, LockupLinearStream};
+use tokei::types::lockup::LockupAmounts;
+// use tokei::tests::mocks::erc721::{ITokeiLockupLinearERC721SnakeDispatcher, ITokeiLockupLinearERC721SnakeDispatcherTrait};
+// use openzeppelin::token::erc721::interface::{ITokeiLockupLinearERC721SnakeDispatcher, ITokeiLockupLinearERC721SnakeDispatcherTrait};
+use tokei::core::interface::{
+ ITokeiLockupLinearERC721SnakeDispatcher, ITokeiLockupLinearERC721SnakeDispatcherTrait
+};
+
+/// TODO: Implement actual test and change the name of this function.
+
+fn create_with_duration() -> (ITokeiLockupLinearDispatcher, ERC20ABIDispatcher, u64) {
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20('Ethereum', 'ETH', 100000000, ADMIN());
+ let recipient_address = RECIPIENT();
+ let approve_token_to = array![ALICE(), BOB(), CHARLIE()];
+ give_tokens_and_approve(
+ approve_token_to, token, token_dispatcher, ADMIN(), tokei.contract_address
+ );
+
+ let (alice, recipient, total_amount, _, cancelable, transferable, range, broker) =
+ Defaults::create_with_durations();
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ let initial_protocol_revenues = tokei.get_protocol_revenues(token);
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+ start_warp(CheatTarget::One(tokei.contract_address), 1000);
+
+ let stream_id = tokei
+ .create_with_duration(
+ ALICE(),
+ recipient_address,
+ total_amount,
+ token,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+ stop_warp(CheatTarget::One(tokei.contract_address));
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ (tokei, token_dispatcher, stream_id)
+}
+
+fn create_with_duration_common(
+ sender: ContractAddress,
+ recipient: ContractAddress,
+ token: ERC20ABIDispatcher,
+ tokei: ITokeiLockupLinearDispatcher
+) -> u64 {
+ let (alice, _, total_amount, _, cancelable, transferable, range, broker) =
+ Defaults::create_with_durations();
+ start_prank(CheatTarget::One(tokei.contract_address), sender);
+ start_warp(CheatTarget::One(tokei.contract_address), 1000);
+
+ let stream_id = tokei
+ .create_with_duration(
+ sender,
+ recipient,
+ total_amount,
+ token.contract_address,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+ stop_warp(CheatTarget::One(tokei.contract_address));
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ stream_id
+}
+#[test]
+fn test_set_protocol_fee() {
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20(
+ 'Ethereum', 'ETH', BoundedInt::max(), ADMIN()
+ );
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ let fee = tokei.get_protocol_fee(token);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ assert(fee == 1, 'Incorrect fee');
+}
+
+#[test]
+fn test_set_nft_descriptor() {
+ let tokei_contract = declare('TokeiLockupLinear');
+ let mut constructor_calldata = array![ADMIN().into()];
+ let random_addr = contract_address_const::<'random'>();
+ let tokei_addr = tokei_contract.deploy(@constructor_calldata).unwrap();
+ // let tokei_address = deploy_tokei(caller_address);
+
+ // Create a role store dispatcher.
+ let tokei = ITokeiLockupLinearDispatcher { contract_address: tokei_addr };
+ start_prank(CheatTarget::One(tokei_addr), ADMIN());
+ tokei.set_nft_descriptor(random_addr);
+ let fee = tokei.get_nft_descriptor();
+ stop_prank(CheatTarget::One(tokei_addr));
+
+ assert(1 == 1, 'Incorrect fee');
+}
+
+
+#[test]
+fn given_normal_conditions_when_create_with_range_then_expected_results() {
+ // *********************************************************************************************
+ // * SETUP *
+ // *********************************************************************************************
+ let caller_address = contract_address_const::<'caller'>();
+
+ let (tokei) = setup(caller_address);
+ let (token_dispatcher, token) = deploy_setup_erc20(
+ 'Ethereum', 'ETH', 100000_u256, caller_address
+ );
+ start_prank(CheatTarget::One(token), caller_address);
+ token_dispatcher.approve(tokei.contract_address, 1000_u256);
+ stop_prank(CheatTarget::One(token));
+
+ // *********************************************************************************************
+ // * TEST LOGIC *
+ // *********************************************************************************************
+
+ // Define variables.
+ let sender = caller_address;
+ let recipient = contract_address_const::<'recipient'>();
+ let total_amount = 1000;
+ let asset = token;
+ let cancelable = true;
+ let transferable = true;
+ let start = 10;
+ let cliff = 100;
+ let end = 1000;
+ let range = Range { start, cliff, end, };
+ let broker_account = caller_address;
+ let broker_fee = 0;
+ let broker = Broker { account: broker_account, fee: broker_fee, };
+
+ prepare_contracts(caller_address, tokei);
+ tokei.set_protocol_fee(asset, PROTOCOL_FEES);
+ // Actual test.
+ let stream_id = tokei
+ .create_with_range(
+ sender, recipient, total_amount, asset, cancelable, transferable, range, broker,
+ );
+
+ // Assertions.
+ assert(stream_id == 1, 'wrong stream id');
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ // Check that the stream nft was minted to the recipient.
+ assert(stream_nft.owner_of(stream_id.into()) == recipient, 'wrong stream nft owner');
+ assert(stream_nft.balance_of(recipient) == 1, 'wrong stream nft balance');
+
+ // *********************************************************************************************
+ // * TEARDOWN *
+ // *********************************************************************************************
+ teardown(tokei);
+}
+
+
+#[test]
+fn test_create_stream_with_range() {
+ // *************************************************************************
+ // EXPECTED RESULTS
+ let expected_stream_id = 1;
+ let expected_ALICE_balance = 990001;
+ let expected_Broker_balance = 3;
+ let expected_protocol_revenue = 1;
+ // *************************************************************************
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20(
+ 'Ethereum', 'ETH', BoundedInt::max(), ADMIN()
+ );
+ let recipient_address = RECIPIENT();
+ let approve_token_to = array![ALICE()];
+ give_tokens_and_approve(
+ approve_token_to, token, token_dispatcher, ADMIN(), tokei.contract_address
+ );
+ let (_, _, total_amount, _, cancelable, transferable, range, broker) =
+ Defaults::create_with_range();
+
+ let balance = token_dispatcher.balance_of(ALICE());
+
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+
+ let initial_protocol_revenues = tokei.get_protocol_revenues(token);
+
+ let ALICE_balance = token_dispatcher.balance_of(ALICE());
+
+ let value = tokei.get_protocol_fee(token);
+
+ let stream_id = tokei
+ .create_with_range(
+ ALICE(),
+ recipient_address,
+ total_amount,
+ token,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ let ALICE_balance = token_dispatcher.balance_of(ALICE());
+
+ let expected_protocol_revenues = initial_protocol_revenues + PROTOCOL_FEES;
+
+ assert(ALICE_balance == expected_ALICE_balance, 'Invalid ALICE balance');
+
+ let Broker_balance = token_dispatcher.balance_of(broker.account);
+ assert(Broker_balance == expected_Broker_balance, 'Invalid Broker balance');
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ let protocol_revenue = tokei.get_protocol_revenues(token);
+ assert(protocol_revenue == expected_protocol_revenue, 'Invalid protocol revenue');
+
+ // Check that the stream nft was minted to the recipient.
+ assert(stream_nft.owner_of(stream_id.into()) == recipient_address, 'wrong stream nft owner');
+
+ assert(stream_nft.balance_of(recipient_address) == 1, 'wrong stream nft balance');
+ assert(stream_id == expected_stream_id, 'Invalid StreamId');
+}
+
+
+#[test]
+fn test_create_with_duration() {
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20(
+ 'Ethereum', 'ETH', BoundedInt::max(), ADMIN()
+ );
+ let recipient_address = RECIPIENT();
+ let approve_token_to = array![ALICE()];
+ give_tokens_and_approve(
+ approve_token_to, token, token_dispatcher, ADMIN(), tokei.contract_address
+ );
+
+ let (alice, recipient, total_amount, _, cancelable, transferable, range, broker) =
+ Defaults::create_with_durations();
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ let initial_protocol_revenues = tokei.get_protocol_revenues(token);
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+ start_warp(CheatTarget::One(tokei.contract_address), 100);
+
+ let stream_id = tokei
+ .create_with_duration(
+ ALICE(),
+ recipient_address,
+ total_amount,
+ token,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+
+ let streamed_amount_of = tokei.streamed_amount_of(stream_id);
+
+ stop_warp(CheatTarget::One(tokei.contract_address));
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ // *************************************************************************
+ // EXPECTED RESULTS
+ let expected_stream_id = 1;
+ let expected_ALICE_balance = 990000;
+ let expected_Broker_balance = 3;
+ let expected_protocol_revenue = 1;
+ let expected_stream = LockupLinearStream {
+ stream_id: 1,
+ sender: ALICE(),
+ asset: token,
+ recipient: RECIPIENT(),
+ start_time: 100,
+ cliff_time: 100 + 2500,
+ end_time: 100 + 10000,
+ is_cancelable: true,
+ was_canceled: false,
+ is_depleted: false,
+ is_stream: true,
+ is_transferable: true,
+ amounts: LockupAmounts { deposited: 9996, withdrawn: 0, refunded: 0, }
+ };
+ // *************************************************************************
+
+ let protocol_revenue = tokei.get_protocol_revenues(token);
+ assert(protocol_revenue == expected_protocol_revenue, 'Invalid protocol revenue');
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ let expected_protocol_revenues = initial_protocol_revenues + PROTOCOL_FEES;
+
+ let protocol_revenue = tokei.get_protocol_revenues(token);
+ let actual_status = tokei.is_warm(stream_id);
+
+ assert(protocol_revenue == expected_protocol_revenue, 'Invalid protocol revenue');
+
+ // Check that the stream nft was minted to the recipient.
+ assert(stream_nft.owner_of(stream_id.into()) == recipient_address, 'wrong stream nft owner');
+ assert(stream_nft.balance_of(recipient_address) == 1, 'wrong stream nft balance');
+ assert(actual_status == true, 'Incorrect status');
+ assert(stream_id == 1, 'wrong stream id');
+ start_warp(CheatTarget::One(tokei.contract_address), 10000);
+ let streamed_amount_of = tokei.streamed_amount_of(stream_id);
+
+ // stop_warp(CheatTarget::One(tokei.contract_address));
+
+ assert(
+ tokei.get_stream(stream_id).amounts.deposited == expected_stream.amounts.deposited,
+ 'Invalid stream'
+ );
+
+ assert(stream_nft.owner_of(stream_id.into()) == recipient_address, 'wrong stream nft owner');
+
+ assert(tokei.get_next_stream_id() == 2, 'Invalid next stream id');
+}
+
+
+#[test]
+#[should_panic(expected: ('deposit amount is zero',))]
+fn test_create_with_duration_when_amount_is_zero() {
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20(
+ 'Ethereum', 'ETH', BoundedInt::max(), ADMIN()
+ );
+ let recipient_address = RECIPIENT();
+ let approve_token_to = array![ALICE()];
+ give_tokens_and_approve(
+ approve_token_to, token, token_dispatcher, ADMIN(), tokei.contract_address
+ );
+
+ let total_amount = 0;
+
+ let (alice, recipient, _, _, cancelable, transferable, range, broker) =
+ Defaults::create_with_durations();
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ let initial_protocol_revenues = tokei.get_protocol_revenues(token);
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+ start_warp(CheatTarget::One(tokei.contract_address), 100);
+
+ let stream_id = tokei
+ .create_with_duration(
+ ALICE(),
+ recipient_address,
+ total_amount,
+ token,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+
+ let streamed_amount_of = tokei.streamed_amount_of(stream_id);
+
+ stop_warp(CheatTarget::One(tokei.contract_address));
+ stop_prank(CheatTarget::One(tokei.contract_address));
+}
+
+#[test]
+#[should_panic(expected: ('start time > cliff time',))]
+fn test_create_with_duration_when_cliff_is_less_than_start() {
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20(
+ 'Ethereum', 'ETH', BoundedInt::max(), ADMIN()
+ );
+ let recipient_address = RECIPIENT();
+ let approve_token_to = array![ALICE()];
+ give_tokens_and_approve(
+ approve_token_to, token, token_dispatcher, ADMIN(), tokei.contract_address
+ );
+
+ let (_, _, total_amount, _, cancelable, transferable, _, broker) =
+ Defaults::create_with_range();
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ let initial_protocol_revenues = tokei.get_protocol_revenues(token);
+
+ let range = Range { start: 1000, cliff: 100, end: 6000, };
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+ start_warp(CheatTarget::One(tokei.contract_address), 1000);
+
+ let stream_id = tokei
+ .create_with_range(
+ ALICE(),
+ recipient_address,
+ total_amount,
+ token,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+
+ let streamed_amount_of = tokei.streamed_amount_of(stream_id);
+
+ stop_warp(CheatTarget::One(tokei.contract_address));
+ stop_prank(CheatTarget::One(tokei.contract_address));
+}
+
+#[test]
+fn test_all_the_getters_with_respect_to_stream() {
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20(
+ 'Ethereum', 'ETH', BoundedInt::max(), ADMIN()
+ );
+ let recipient_address = RECIPIENT();
+ let approve_token_to = array![ALICE()];
+ give_tokens_and_approve(
+ approve_token_to, token, token_dispatcher, ADMIN(), tokei.contract_address
+ );
+
+ let (alice, recipient, total_amount, _, cancelable, transferable, range, broker) =
+ Defaults::create_with_durations();
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ let initial_protocol_revenues = tokei.get_protocol_revenues(token);
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+ start_warp(CheatTarget::One(tokei.contract_address), 100);
+
+ let stream_id = tokei
+ .create_with_duration(
+ ALICE(),
+ recipient_address,
+ total_amount,
+ token,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+
+ let get_start_time = tokei.get_start_time(stream_id);
+ assert(get_start_time == 100, 'Incorrect start time');
+ let get_asset = tokei.get_asset(stream_id);
+ assert(get_asset == token, 'Incorrect asset');
+ let get_recipient = tokei.get_recipient(stream_id);
+ assert(get_recipient == recipient_address, 'Incorrect recipient');
+ let get_cliff_time = tokei.get_cliff_time(stream_id);
+ assert(get_cliff_time == 100 + 2500, 'Incorrect cliff time');
+ let get_deposited_amount = tokei.get_deposited_amount(stream_id);
+ assert(get_deposited_amount == 9996, 'Incorrect deposited amount');
+ let get_end_time = tokei.get_end_time(stream_id);
+ assert(get_end_time == 100 + 4000, 'Incorrect end time');
+ let get_range = tokei.get_range(stream_id);
+ assert(get_range.start == 100, 'Incorrect start');
+ assert(get_range.cliff == 100 + 2500, 'Incorrect cliff');
+ assert(get_range.end == 100 + 4000, 'Incorrect end');
+
+ let get_refunded_amount = tokei.get_refunded_amount(stream_id);
+ assert(get_refunded_amount == 0, 'Incorrect refunded amount');
+ let get_sender = tokei.get_sender(stream_id);
+ assert(get_sender == ALICE(), 'Incorrect sender');
+ let get_withdrawn_amount = tokei.get_withdrawn_amount(stream_id);
+ assert(get_withdrawn_amount == 0, 'Incorrect withdrawn amount');
+ let is_cancelable = tokei.is_cancelable(stream_id);
+ assert(is_cancelable == true, 'Incorrect cancelable');
+ let is_transferable = tokei.is_transferable(stream_id);
+ assert(is_transferable == true, 'Incorrect transferable');
+ let is_depleted = tokei.is_depleted(stream_id);
+ assert(is_depleted == false, 'Incorrect depleted');
+ let is_stream = tokei.is_stream(stream_id);
+ assert(is_stream == true, 'Incorrect stream');
+ let refundable_amount_of = tokei.refundable_amount_of(stream_id);
+ assert(refundable_amount_of == 9996, 'Incorrect refundable amount');
+ let get_refunded_amount = tokei.get_refunded_amount(stream_id);
+ assert(get_refunded_amount == 0, 'Incorrect refunded amount');
+ let is_cold = tokei.is_cold(stream_id);
+ assert(is_cold == false, 'Incorrect cold');
+ let is_warm = tokei.is_warm(stream_id);
+ assert(is_warm == true, 'Incorrect warm');
+ let was_canceled = tokei.was_canceled(stream_id);
+ assert(was_canceled == false, 'Incorrect was canceled');
+}
+
+
+#[test]
+fn test_get_cliff_time() {
+ let (tokei, token, stream_id) = create_with_duration();
+ let cliff_time = tokei.get_cliff_time(stream_id);
+ let expected_cliff_time = 1000 + 2500;
+
+ assert(cliff_time == expected_cliff_time, 'Invalid cliff time');
+}
+
+#[test]
+fn test_get_cliff_time_when_null() {
+ let (tokei, token, _) = create_with_duration();
+ let stream_id = 102;
+ let cliff_time = tokei.get_cliff_time(stream_id);
+ let expected_cliff_time = 0;
+
+ assert(cliff_time == expected_cliff_time, 'Invalid cliff time');
+}
+
+#[test]
+fn test_get_range() {
+ let (tokei, token, stream_id) = create_with_duration();
+ let range = tokei.get_range(stream_id);
+ let expected_range = Range { start: 1000, cliff: 1000 + 2500, end: 1000 + 4000, };
+
+ assert(range == expected_range, 'Invalid range');
+}
+
+#[test]
+fn test_get_range_when_null() {
+ let (tokei, token, _) = create_with_duration();
+ let stream_id = 102;
+ let range = tokei.get_range(stream_id);
+ let expected_range = Range { start: 0, cliff: 0, end: 0, };
+
+ assert(range == expected_range, 'Invalid range');
+}
+
+#[test]
+fn test_get_stream_when_status_settled() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_warp(CheatTarget::One(tokei.contract_address), 1000000);
+
+ let actual_stream = tokei.get_stream(stream_id);
+ let expected_stream = LockupLinearStream {
+ stream_id: 1,
+ sender: ALICE(),
+ asset: token.contract_address,
+ recipient: RECIPIENT(),
+ start_time: 1000,
+ cliff_time: 1000 + 2500,
+ end_time: 1000 + 4000,
+ is_cancelable: false,
+ was_canceled: false,
+ is_depleted: false,
+ is_stream: true,
+ is_transferable: true,
+ amounts: LockupAmounts { deposited: 9996, withdrawn: 0, refunded: 0, }
+ };
+
+ assert(actual_stream == expected_stream, 'Invalid stream');
+}
+
+#[test]
+fn test_get_stream_when_not_settled() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_warp(CheatTarget::One(tokei.contract_address), 10000);
+
+ let actual_stream = tokei.get_stream(stream_id);
+
+ let expected_stream = LockupLinearStream {
+ stream_id: 1,
+ sender: ALICE(),
+ asset: token.contract_address,
+ recipient: RECIPIENT(),
+ start_time: 1000,
+ cliff_time: 1000 + 2500,
+ end_time: 1000 + 4000,
+ is_cancelable: false,
+ was_canceled: false,
+ is_depleted: false,
+ is_stream: true,
+ is_transferable: true,
+ amounts: LockupAmounts { deposited: 9996, withdrawn: 0, refunded: 0, }
+ };
+
+ assert(actual_stream == expected_stream, 'Invalid stream');
+}
+
+#[test]
+fn test_streamed_amount_of_cliff_time_in_past() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ let actual_streamed_amount = tokei.streamed_amount_of(stream_id);
+
+ let expected_streamed_amount = 0;
+
+ assert(actual_streamed_amount == expected_streamed_amount, 'Invalid streamed amount');
+}
+
+#[test]
+fn test_streamed_amount_of_cliff_time_in_present() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 4000);
+
+ let actual_streamed_amount = tokei.streamed_amount_of(stream_id);
+
+ let expected_streamed_amount = 7497;
+
+ assert(actual_streamed_amount == expected_streamed_amount, 'Invalid streamed amount');
+}
+
+#[test]
+fn test_streamed_amount_of_cliff_time_in_present_1() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let actual_streamed_amount = tokei.streamed_amount_of(stream_id);
+
+ let expected_streamed_amount = 9996;
+
+ assert(actual_streamed_amount == expected_streamed_amount, 'Invalid streamed amount');
+}
+
+#[test]
+fn test_withdrawable_amount_of_cliff_time() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let withdrawable_amount_of = tokei.withdrawable_amount_of(stream_id);
+
+ assert(withdrawable_amount_of == 9996, 'Invalid withdrawable amount');
+}
+
+#[test]
+fn test_withdraw_by_recipient() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let withdrawable_amount_of = tokei.withdrawable_amount_of(stream_id);
+
+ assert(withdrawable_amount_of == 9996, 'Invalid withdrawable amount');
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+
+ tokei.withdraw(stream_id, RECIPIENT(), withdrawable_amount_of);
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 9996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+}
+
+#[test]
+fn test_withdraw_by_recipient_before_total_time() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 3600);
+
+ let withdrawable_amount_of = tokei.withdrawable_amount_of(stream_id);
+ assert(withdrawable_amount_of == 6497, 'Invalid withdrawable amount');
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+
+ tokei.withdraw(stream_id, RECIPIENT(), withdrawable_amount_of);
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 6497;
+ assert(balance == expected_balance, 'Invalid balance');
+}
+
+#[test]
+fn test_withdraw_by_approved_caller() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let withdrawable_amount_of = tokei.withdrawable_amount_of(stream_id);
+
+ assert(withdrawable_amount_of == 9996, 'Invalid withdrawable amount');
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+ stream_nft.approve(BOB(), stream_id.into());
+
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ start_prank(CheatTarget::One(tokei.contract_address), BOB());
+
+ tokei.withdraw(stream_id, RECIPIENT(), withdrawable_amount_of);
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 9996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+}
+
+#[test]
+fn test_withdraw_by_caller() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let withdrawable_amount_of = tokei.withdrawable_amount_of(stream_id);
+ assert(withdrawable_amount_of == 9996, 'Invalid withdrawable amount');
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+
+ tokei.withdraw(stream_id, RECIPIENT(), withdrawable_amount_of);
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 9996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+}
+#[test]
+#[should_panic(expected: ('invalid sender withdrawal',))]
+fn test_withdraw_by_approved_caller_to_other_address_than_recipient() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let withdrawable_amount_of = tokei.withdrawable_amount_of(stream_id);
+
+ assert(withdrawable_amount_of == 9996, 'Invalid withdrawable amount');
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+ stream_nft.approve(BOB(), stream_id.into());
+
+ let addr = stream_nft.owner_of(stream_id.into());
+
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ start_prank(CheatTarget::One(tokei.contract_address), BOB());
+
+ tokei.withdraw(stream_id, BOB(), withdrawable_amount_of);
+}
+
+#[test]
+#[should_panic(expected: ('lockup_unauthorized',))]
+fn test_withdraw_by_unapproved_caller() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let withdrawable_amount_of = tokei.withdrawable_amount_of(stream_id);
+
+ assert(withdrawable_amount_of == 9996, 'Invalid withdrawable amount');
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), BOB());
+
+ tokei.withdraw(stream_id, RECIPIENT(), withdrawable_amount_of);
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 9996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+}
+
+#[test]
+fn test_withdraw_max() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+
+ tokei.withdraw_max(stream_id, RECIPIENT());
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 9996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+}
+
+#[test]
+fn test_withdraw_max_and_transfer() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ let recipient_nft_balance_before = stream_nft.balance_of(RECIPIENT());
+ assert(recipient_nft_balance_before == 1, 'Invalid nft balance');
+ let bob_nft_balance_before = stream_nft.balance_of(BOB());
+ assert(bob_nft_balance_before == 0, 'Invalid nft balance');
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+
+ tokei.withdraw_max_and_transfer(stream_id, BOB());
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 9996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+
+ let recipient_nft_balance_after = stream_nft.balance_of(RECIPIENT());
+ assert(recipient_nft_balance_after == 0, 'Invalid nft balance');
+
+ let bob_nft_balance_after = stream_nft.balance_of(BOB());
+ assert(bob_nft_balance_after == 1, 'Invalid nft balance');
+}
+
+#[test]
+#[should_panic(expected: ('Stream is not transferable',))]
+fn test_withdraw_max_and_transfer_when_not_transferable() {
+ let transferable = false;
+ let (tokei) = setup(ADMIN());
+ let (token_dispatcher, token) = deploy_setup_erc20('Ethereum', 'ETH', 100000000, ADMIN());
+ let recipient_address = RECIPIENT();
+ let approve_token_to = array![ALICE()];
+ give_tokens_and_approve(
+ approve_token_to, token, token_dispatcher, ADMIN(), tokei.contract_address
+ );
+
+ let (alice, recipient, total_amount, _, cancelable, _, range, broker) =
+ Defaults::create_with_durations();
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_protocol_fee(token, PROTOCOL_FEES);
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ let initial_protocol_revenues = tokei.get_protocol_revenues(token);
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+ start_warp(CheatTarget::One(tokei.contract_address), 1000);
+
+ let stream_id = tokei
+ .create_with_duration(
+ ALICE(),
+ recipient_address,
+ total_amount,
+ token,
+ cancelable,
+ transferable,
+ range,
+ broker,
+ );
+ stop_warp(CheatTarget::One(tokei.contract_address));
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ start_warp(CheatTarget::One(tokei.contract_address), 5000);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ let recipient_nft_balance_before = stream_nft.balance_of(RECIPIENT());
+ assert(recipient_nft_balance_before == 1, 'Invalid nft balance');
+ let bob_nft_balance_before = stream_nft.balance_of(BOB());
+ assert(bob_nft_balance_before == 0, 'Invalid nft balance');
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+
+ tokei.withdraw_max_and_transfer(stream_id, BOB());
+}
+
+#[test]
+fn test_withdraw_multiple() {
+ let (_, _, _, _, _, _, _, broker) = Defaults::create_with_durations();
+
+ let total_amount_2 = 10000;
+ let cancelable_2 = true;
+ let transferable_2 = true;
+ let durations_2 = Durations { cliff: 2500, total: 6000, };
+
+ let total_amount_3 = 12000;
+ let cancelable_3 = false;
+ let transferable_3 = false;
+ let durations_3 = Durations { cliff: 4000, total: 6500, };
+
+ let reciever = contract_address_const::<'reciever'>();
+
+ let (tokei, token, stream_id_1) = create_with_duration();
+
+ start_prank(CheatTarget::One(tokei.contract_address), BOB());
+ start_warp(CheatTarget::One(tokei.contract_address), 1000);
+
+ let stream_id_2 = tokei
+ .create_with_duration(
+ BOB(),
+ RECIPIENT(),
+ total_amount_2,
+ token.contract_address,
+ cancelable_2,
+ transferable_2,
+ durations_2,
+ broker,
+ );
+
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ start_prank(CheatTarget::One(tokei.contract_address), CHARLIE());
+ let stream_id_3 = tokei
+ .create_with_duration(
+ CHARLIE(),
+ RECIPIENT(),
+ total_amount_3,
+ token.contract_address,
+ cancelable_3,
+ transferable_3,
+ durations_3,
+ broker,
+ );
+
+ stop_warp(CheatTarget::One(tokei.contract_address));
+
+ start_warp(CheatTarget::One(tokei.contract_address), 10000);
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ let recipient_nft_balance_before = stream_nft.balance_of(RECIPIENT());
+ assert(recipient_nft_balance_before == 3, 'Invalid nft balance');
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+ stream_nft.approve(reciever, stream_id_1.into());
+ stream_nft.approve(reciever, stream_id_2.into());
+ stream_nft.approve(reciever, stream_id_3.into());
+ stop_prank(CheatTarget::One(tokei.contract_address));
+
+ start_prank(CheatTarget::One(tokei.contract_address), reciever);
+ let stream_ids = array![stream_id_1, stream_id_2, stream_id_3];
+ let amounts = array![9996, 6000, 11000];
+ tokei.withdraw_multiple(stream_ids.span(), RECIPIENT(), amounts.span(),);
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 26996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+ assert(tokei.get_protocol_revenues(token.contract_address) == 3, 'Invalid protocol revenue');
+
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ let admin_balance_before = token.balance_of(ADMIN());
+ tokei.claim_protocol_revenues(token.contract_address);
+ let admin_balance_after = token.balance_of(ADMIN());
+ let expected_admin_balance = admin_balance_before + 3;
+ assert(admin_balance_after == expected_admin_balance, 'Invalid admin balance');
+}
+
+#[test]
+fn test_burn_token_when_depleted() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 8000);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+
+ tokei.withdraw_max(stream_id, RECIPIENT());
+
+ let balance = token.balance_of(RECIPIENT());
+ let expected_balance = 9996_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+
+ let old_tokei_nft_balance = stream_nft.balance_of(RECIPIENT());
+ assert(old_tokei_nft_balance == 1, 'Invalid nft balance');
+
+ tokei.burn_token(stream_id);
+
+ let new_tokei_nft_balance = stream_nft.balance_of(RECIPIENT());
+ assert(new_tokei_nft_balance == 0, 'Invalid nft balance');
+}
+
+#[test]
+#[should_panic(expected: ('stream has not depleted',))]
+fn test_burn_token_when_not_depleted() {
+ let (tokei, token, stream_id) = create_with_duration();
+ start_warp(CheatTarget::One(tokei.contract_address), 4100);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+
+ tokei.withdraw_max(stream_id, RECIPIENT());
+
+ let balance = token.balance_of(RECIPIENT());
+
+ let expected_balance = 7796_u256;
+ assert(balance == expected_balance, 'Invalid balance');
+
+ let old_tokei_nft_balance = stream_nft.balance_of(RECIPIENT());
+ assert(old_tokei_nft_balance == 1, 'Invalid nft balance');
+
+ tokei.burn_token(stream_id);
+
+ let new_tokei_nft_balance = stream_nft.balance_of(RECIPIENT());
+ assert(new_tokei_nft_balance == 0, 'Invalid nft balance');
+}
+
+#[test]
+fn test_cancel() {
+ let (tokei, token, stream_id) = create_with_duration();
+ let before_balance_of_sender = token.balance_of(ALICE());
+
+ start_warp(CheatTarget::One(tokei.contract_address), 4100);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+ let stream = tokei.get_stream(stream_id);
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+ tokei.cancel(stream_id);
+
+ let after_balance_of_sender = token.balance_of(ALICE());
+
+ assert(before_balance_of_sender != after_balance_of_sender, 'Balance did not change');
+ assert(tokei.is_cancelable(stream_id) == false, 'Invalid stream');
+}
+
+#[test]
+#[should_panic(expected: ('stream_settled',))]
+fn test_cancel_should_panic() {
+ let (tokei, token, stream_id) = create_with_duration();
+ let before_balance_of_sender = token.balance_of(ALICE());
+
+ start_warp(CheatTarget::One(tokei.contract_address), 7000);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+ let stream = tokei.get_stream(stream_id);
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+ tokei.cancel(stream_id);
+}
+
+#[test]
+fn test_renounce() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_warp(CheatTarget::One(tokei.contract_address), 4100);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), ALICE());
+ tokei.renounce(stream_id);
+
+ assert(tokei.is_cancelable(stream_id) == false, 'Invalid stream');
+}
+
+#[test]
+#[should_panic(expected: ('lockup_unauthorized',))]
+fn test_renounce_by_recipient() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_warp(CheatTarget::One(tokei.contract_address), 4100);
+
+ let stream_nft = ITokeiLockupLinearERC721SnakeDispatcher {
+ contract_address: tokei.contract_address
+ };
+
+ start_prank(CheatTarget::One(tokei.contract_address), RECIPIENT());
+ tokei.renounce(stream_id);
+
+ assert(tokei.is_cancelable(stream_id) == false, 'Invalid stream');
+}
+
+#[test]
+fn test_transfer_admin() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.transfer_admin(BOB());
+
+ assert(tokei.get_admin() == BOB(), 'Invalid admin');
+}
+
+#[test]
+fn test_get_streams_by_sender() {
+ // create_with_duration function passed Alice as sender and recipient as RECIPIENT
+ let (tokei, token, stream_id_1) = create_with_duration();
+ let stream_id_2 = create_with_duration_common(ALICE(), RECIPIENT(), token, tokei);
+ let stream_id_3 = create_with_duration_common(BOB(), CHARLIE(), token, tokei);
+
+ let streams_alice = tokei.get_streams_by_sender(ALICE());
+ assert(streams_alice.len() == 2, 'Invalid stream count');
+
+ let streams_bob = tokei.get_streams_by_sender(BOB());
+ assert(streams_bob.len() == 1, 'Invalid stream count');
+
+ let stream_alice_1 = tokei.get_stream(stream_id_1);
+
+ let stream_alice_2 = tokei.get_stream(stream_id_2);
+ let stream_bob_1 = tokei.get_stream(stream_id_3);
+
+ assert(*streams_alice.at(0) == stream_alice_1, 'Invalid stream');
+ assert(*streams_alice.at(1) == stream_alice_2, 'Invalid stream');
+ assert(*streams_bob.at(0) == stream_bob_1, 'Invalid stream');
+}
+#[test]
+fn test_get_streams_by_receiver() {
+ // create_with_duration function passed Alice as sender and recipient as RECIPIENT
+ let (tokei, token, stream_id_1) = create_with_duration();
+ let stream_id_2 = create_with_duration_common(ALICE(), RECIPIENT(), token, tokei);
+ let stream_id_3 = create_with_duration_common(BOB(), CHARLIE(), token, tokei);
+
+ let streams_recipient = tokei.get_streams_by_recipient(RECIPIENT());
+ let streams_charlie = tokei.get_streams_by_recipient(CHARLIE());
+
+ let stream_1 = tokei.get_stream(stream_id_1);
+ let stream_2 = tokei.get_stream(stream_id_2);
+ let stream_3 = tokei.get_stream(stream_id_3);
+
+ assert(streams_recipient.len() == 2, 'Invalid stream count');
+ assert(streams_charlie.len() == 1, 'Invalid stream count');
+ assert(*streams_recipient.at(0) == stream_1, 'Invalid stream');
+ assert(*streams_recipient.at(1) == stream_2, 'Invalid stream');
+ assert(*streams_charlie.at(0) == stream_3, 'Invalid stream');
+}
+
+#[test]
+fn test_get_stream_ids_by_receiver() {
+ // create_with_duration function passed Alice as sender and recipient as RECIPIENT
+ let (tokei, token, stream_id_1) = create_with_duration();
+ let stream_id_2 = create_with_duration_common(ALICE(), RECIPIENT(), token, tokei);
+ let stream_id_3 = create_with_duration_common(BOB(), CHARLIE(), token, tokei);
+
+ let stream_ids_recipient = tokei.get_streams_ids_by_recipient(RECIPIENT());
+ let stream_ids_charlie = tokei.get_streams_ids_by_recipient(CHARLIE());
+
+ let expected_stream_id_1 = 1_u64;
+ let expected_stream_id_2 = 2_u64;
+ let expected_stream_id_3 = 3_u64;
+
+ assert(stream_ids_recipient.len() == 2, 'Invalid stream count');
+ assert(stream_ids_charlie.len() == 1, 'Invalid stream count');
+ assert(*stream_ids_recipient.at(0) == expected_stream_id_1, 'Invalid stream');
+ assert(*stream_ids_recipient.at(1) == expected_stream_id_2, 'Invalid stream');
+ assert(*stream_ids_charlie.at(0) == expected_stream_id_3, 'Invalid stream');
+}
+
+#[test]
+fn test_get_stream_ids_by_sender() {
+ // create_with_duration function passed Alice as sender and recipient as RECIPIENT
+ let (tokei, token, stream_id_1) = create_with_duration();
+ let stream_id_2 = create_with_duration_common(ALICE(), RECIPIENT(), token, tokei);
+ let stream_id_3 = create_with_duration_common(BOB(), CHARLIE(), token, tokei);
+
+ let stream_ids_alice = tokei.get_streams_ids_by_sender(ALICE());
+ let stream_ids_bob = tokei.get_streams_ids_by_sender(BOB());
+
+ let expected_stream_id_1 = 1_u64;
+ let expected_stream_id_2 = 2_u64;
+ let expected_stream_id_3 = 3_u64;
+
+ assert(stream_ids_alice.len() == 2, 'Invalid stream count');
+ assert(stream_ids_bob.len() == 1, 'Invalid stream count');
+ assert(*stream_ids_alice.at(0) == expected_stream_id_1, 'Invalid stream');
+ assert(*stream_ids_alice.at(1) == expected_stream_id_2, 'Invalid stream');
+ assert(*stream_ids_bob.at(0) == expected_stream_id_3, 'Invalid stream');
+}
+
+#[test]
+#[should_panic(expected: ('lockup_unauthorized',))]
+fn test_set_protocol_fee_panic() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_prank(CheatTarget::One(tokei.contract_address), BOB());
+ tokei.set_protocol_fee(token.contract_address, 100);
+}
+
+#[test]
+fn test_set_flash_fee() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_prank(CheatTarget::One(tokei.contract_address), ADMIN());
+ tokei.set_flash_fee(100);
+
+ assert(tokei.get_flash_fee() == 100, 'Invalid protocol fee');
+}
+
+#[test]
+#[should_panic(expected: ('lockup_unauthorized',))]
+fn test_set_flash_fee_panic() {
+ let (tokei, token, stream_id) = create_with_duration();
+
+ start_prank(CheatTarget::One(tokei.contract_address), BOB());
+ tokei.set_flash_fee(100);
+}
+
+
+impl LockupLinearStreamPrintTrait of PrintTrait {
+ fn print(self: LockupLinearStream) {
+ let message = array![
+ 'LockupLinearStream: ',
+ self.sender.into(),
+ self.asset.into(),
+ self.start_time.into(),
+ self.cliff_time.into(),
+ self.end_time.into(),
+ self.is_cancelable.into(),
+ self.was_canceled.into(),
+ self.is_depleted.into(),
+ self.is_stream.into(),
+ self.is_transferable.into(),
+ ];
+ message.print();
+ }
+}
diff --git a/src/tests/utils/defaults.cairo b/src/tests/utils/defaults.cairo
new file mode 100644
index 0000000..a74e6ef
--- /dev/null
+++ b/src/tests/utils/defaults.cairo
@@ -0,0 +1,107 @@
+mod Defaults {
+ use starknet::{
+ ContractAddress, get_caller_address, Felt252TryIntoContractAddress, contract_address_const,
+ ClassHash,
+ };
+ use tokei::types::lockup_linear::{LockupLinearStream, Range, Durations, Broker};
+ use tokei::types::lockup::{LockupAmounts, CreateAmounts};
+ use tokei::tests::utils::utils::Utils::{
+ pow_256, ADMIN, BROKER, RECIPIENT, ASSET, ALICE, setup, teardown, prepare_contracts,
+ deploy_setup_erc20, deploy_tokei
+ };
+
+
+ const BROKER_FEE: u256 = 3; //0.03%
+ const BROKER_FEE_AMOUNT: u256 = 30_120_481_927_710_843_373_000;
+ const CLIFF_AMOUNT: u256 = 2_500_000_000_000_000_000_000;
+ const CLIFF_DURATION: u64 = 2500;
+ const DEPOSIT_AMOUNT: u256 = 10_000_000_000_000_000_000_000;
+ const PROTOCOL_FEES: u256 = 1; //0.01%
+ const PROTOCOL_FEE_AMOUNT: u256 = 10_040_160_642_570_281_124_000;
+ const REFUND_AMOUNT: u256 = 7_500_000_000_000_000_000_000;
+ const TOTAL_AMOUNT: u256 = 10_000;
+ const TOTAL_DURATION: u64 = 4000;
+ const WITHDRAW_AMOUNT: u256 = 2_600_000_000_000_000_000_000;
+ const MAY_1_2023: u64 = 1_682_899_200;
+
+
+ fn setup_1() -> (u64, u64, u64) {
+ let START_TIME = MAY_1_2023 + 172_800;
+ let CLIFF_TIME = START_TIME + CLIFF_DURATION;
+ let END_TIME = START_TIME + TOTAL_DURATION;
+
+ (START_TIME, CLIFF_TIME, END_TIME)
+ }
+
+ fn lockup_amounts() -> LockupAmounts {
+ LockupAmounts { deposited: DEPOSIT_AMOUNT, refunded: 0, withdrawn: 0, }
+ }
+
+ fn broker() -> Broker {
+ Broker { account: BROKER(), fee: BROKER_FEE, }
+ }
+
+ fn durations() -> Durations {
+ Durations { cliff: CLIFF_DURATION, total: TOTAL_DURATION, }
+ }
+
+ fn lockup_create_amounts() -> CreateAmounts {
+ CreateAmounts {
+ deposit: DEPOSIT_AMOUNT,
+ protocol_fee: PROTOCOL_FEE_AMOUNT,
+ broker_fee: BROKER_FEE_AMOUNT,
+ }
+ }
+
+ fn lockup_linear_range() -> Range {
+ let (START_TIME, CLIFF_TIME, END_TIME) = setup_1();
+ Range { start: START_TIME, cliff: CLIFF_TIME, end: END_TIME, }
+ }
+
+ fn lockup_linear_stream() -> LockupLinearStream {
+ let (START_TIME, CLIFF_TIME, END_TIME) = setup_1();
+ LockupLinearStream {
+ stream_id: 1,
+ sender: contract_address_const::<'sender'>(),
+ asset: contract_address_const::<'asset'>(),
+ recipient: contract_address_const::<'recipient'>(),
+ start_time: START_TIME,
+ cliff_time: CLIFF_TIME,
+ end_time: END_TIME,
+ is_cancelable: true,
+ was_canceled: false,
+ is_depleted: false,
+ is_stream: true,
+ is_transferable: true,
+ amounts: lockup_amounts(),
+ }
+ }
+
+ fn create_with_durations() -> (
+ ContractAddress, ContractAddress, u256, ContractAddress, bool, bool, Durations, Broker
+ ) {
+ // let asset = contract_address_const::<'asset'>();
+ let broker = broker();
+ let cancelable = true;
+ let durations = durations();
+ // let recipient = contract_address_const::<'recipient'>();
+ // let sender = contract_address_const::<'sender'>();
+ let total_amount = TOTAL_AMOUNT;
+
+ (ALICE(), RECIPIENT(), total_amount, ASSET(), cancelable, true, durations, broker)
+ }
+
+ fn create_with_range() -> (
+ ContractAddress, ContractAddress, u256, ContractAddress, bool, bool, Range, Broker
+ ) {
+ // let asset = contract_address_const::<'asset'>();
+ let broker = broker();
+ let cancelable = true;
+ let range = lockup_linear_range();
+ // let recipient = contract_address_const::<'recipient'>();
+ // let sender = contract_address_const::<'sender'>();
+ let total_amount = TOTAL_AMOUNT;
+
+ (ALICE(), RECIPIENT(), total_amount, ASSET(), cancelable, true, range, broker)
+ }
+}
diff --git a/src/tests/utils/utils.cairo b/src/tests/utils/utils.cairo
new file mode 100644
index 0000000..aa4abc4
--- /dev/null
+++ b/src/tests/utils/utils.cairo
@@ -0,0 +1,182 @@
+use starknet::ContractAddress;
+
+mod Utils {
+ use core::traits::TryInto;
+ use starknet::ContractAddress;
+ use snforge_std::{
+ declare, ContractClassTrait, start_prank, stop_prank, start_spoof, CheatTarget, TxInfoMock,
+ get_class_hash, ContractClass
+ };
+
+ use openzeppelin::token::erc20::interface::{
+ IERC20, IERC20Metadata, ERC20ABIDispatcher, ERC20ABIDispatcherTrait
+ };
+
+ // Local imports.
+ use tokei::core::lockup_linear::{
+ ITokeiLockupLinearDispatcher, ITokeiLockupLinearDispatcherTrait
+ };
+
+
+ fn pow_256(self: u256, mut exponent: u8) -> u256 {
+ if self.is_zero() {
+ return 0;
+ }
+ let mut result = 1;
+ let mut base = self;
+
+ loop {
+ if exponent & 1 == 1 {
+ result = result * base;
+ }
+
+ exponent = exponent / 2;
+ if exponent == 0 {
+ break result;
+ }
+
+ base = base * base;
+ }
+ }
+
+ fn OWNER() -> ContractAddress {
+ 'owner'.try_into().unwrap()
+ }
+
+ fn RECIPIENT() -> ContractAddress {
+ 'recipient'.try_into().unwrap()
+ }
+
+ fn SPENDER() -> ContractAddress {
+ 'spender'.try_into().unwrap()
+ }
+
+ fn ALICE() -> ContractAddress {
+ 'alice'.try_into().unwrap()
+ }
+
+ fn BOB() -> ContractAddress {
+ 'bob'.try_into().unwrap()
+ }
+
+ fn CHARLIE() -> ContractAddress {
+ 'charlie'.try_into().unwrap()
+ }
+
+ fn ADMIN() -> ContractAddress {
+ 'admin'.try_into().unwrap()
+ }
+
+ fn ASSET() -> ContractAddress {
+ 'asset'.try_into().unwrap()
+ }
+ fn BROKER() -> ContractAddress {
+ 'broker'.try_into().unwrap()
+ }
+
+ /// Utility function to deploy a `TokeiLockupLinear` contract and return its address.
+ fn deploy_tokei(
+ initial_admin: ContractAddress
+ ) -> (ContractAddress, ITokeiLockupLinearDispatcher) {
+ let tokei_contract = declare('TokeiLockupLinear');
+ let mut constructor_calldata = array![initial_admin.into()];
+ let tokei_addr = tokei_contract.deploy(@constructor_calldata).unwrap();
+ // let tokei_address = deploy_tokei(caller_address);
+
+ // Create a role store dispatcher.
+ let tokei = ITokeiLockupLinearDispatcher { contract_address: tokei_addr };
+ // start
+
+ (tokei_addr, tokei,)
+ }
+
+ fn deploy_setup_erc20(
+ name: felt252, symbol: felt252, initial_supply: u256, recipient: ContractAddress
+ ) -> (ERC20ABIDispatcher, ContractAddress) {
+ let token_contract = declare('ERC20');
+ let mut calldata = array![name, symbol];
+ Serde::serialize(@initial_supply, ref calldata);
+ Serde::serialize(@recipient, ref calldata);
+ let token_addr = token_contract.deploy(@calldata).unwrap();
+ let token_dispatcher = ERC20ABIDispatcher { contract_address: token_addr };
+
+ (token_dispatcher, token_addr)
+ }
+
+ // Utility function to prank the caller address
+ fn prepare_contracts(caller_address: ContractAddress, tokei: ITokeiLockupLinearDispatcher,) {
+ // Prank the caller address for calls to `TokeiLockupLinear` contract.
+ start_prank(CheatTarget::One(tokei.contract_address), caller_address);
+ }
+
+ /// Utility function to teardown the test environment.
+ fn teardown(tokei: ITokeiLockupLinearDispatcher,) {
+ stop_prank(CheatTarget::One(tokei.contract_address));
+ }
+
+ /// Utility function to setup the test environment.
+ fn setup(caller_address: ContractAddress) -> (ITokeiLockupLinearDispatcher,) {
+ // Setup the contracts.
+ let (tokei_addr, tokei,) = deploy_tokei(caller_address);
+ // Prank the caller address.
+ // prepare_contracts(ADMIN(), tokei,);
+ // tokei.set_protocol_fee(1);
+ // Return the caller address and the contract interfaces.
+ (tokei,)
+ }
+
+ fn give_tokens(
+ recipients: Array,
+ token_adrr: ContractAddress,
+ token_disptacher: ERC20ABIDispatcher,
+ owner: ContractAddress
+ ) {
+ start_prank(CheatTarget::One(token_adrr), owner);
+
+ let mut i = 0;
+ loop {
+ if (i >= recipients.len()) {
+ break;
+ }
+
+ let address = *recipients.at(i);
+ let amount = 10000 * pow_256(10, 18);
+
+ ERC20ABIDispatcher { contract_address: token_adrr }.transfer(address, amount.into());
+ stop_prank(CheatTarget::One(token_adrr));
+
+ i += 1;
+ };
+ }
+
+ fn give_tokens_and_approve(
+ recipients: Array,
+ token_adrr: ContractAddress,
+ token_disptacher: ERC20ABIDispatcher,
+ owner: ContractAddress,
+ tokei_addr: ContractAddress,
+ ) {
+ let mut i = 0;
+ loop {
+ if (i >= recipients.len()) {
+ break;
+ }
+
+ let address = *recipients.at(i);
+ let amount = 1000000;
+ start_prank(CheatTarget::One(token_adrr), owner);
+ ERC20ABIDispatcher { contract_address: token_adrr }.transfer(address, amount.into());
+ stop_prank(CheatTarget::One(token_adrr));
+
+ start_prank(CheatTarget::One(token_adrr), address);
+ let amount = 1000000;
+
+ ERC20ABIDispatcher { contract_address: token_adrr }.approve(tokei_addr, amount.into());
+
+ stop_prank(CheatTarget::One(token_adrr));
+
+ i += 1;
+ };
+ }
+}
+
diff --git a/src/tokens/erc721.cairo b/src/tokens/erc721.cairo
deleted file mode 100644
index d021858..0000000
--- a/src/tokens/erc721.cairo
+++ /dev/null
@@ -1,308 +0,0 @@
-// Copied from OpenZeppelin Contracts
-// https://github.com/OpenZeppelin/cairo-contracts/blob/cairo-2/src/token/erc721/interface.cairo
-
-use array::SpanTrait;
-use starknet::ContractAddress;
-
-const IERC721_ID: felt252 = 0x33eb2f84c309543403fd69f0d0f363781ef06ef6faeb0131ff16ea3175bd943;
-const IERC721_METADATA_ID: felt252 =
- 0x6069a70848f907fa57668ba1875164eb4dcee693952468581406d131081bbd;
-const IERC721_RECEIVER_ID: felt252 =
- 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc;
-
-#[starknet::interface]
-trait IERC721 {
- fn initializer(ref self: TState, name_: felt252, symbol_: felt252, admin: ContractAddress);
- fn balance_of(self: @TState, account: ContractAddress) -> u128;
- fn owner_of(self: @TState, token_id: u128) -> ContractAddress;
- fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u128);
- fn safe_transfer_from(
- ref self: TState,
- from: ContractAddress,
- to: ContractAddress,
- token_id: u128,
- data: Span
- );
- fn approve(ref self: TState, to: ContractAddress, token_id: u128);
- fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool);
- fn get_approved(self: @TState, token_id: u128) -> ContractAddress;
- fn is_approved_for_all(
- self: @TState, owner: ContractAddress, operator: ContractAddress
- ) -> bool;
- fn mint(ref self: TState, to: ContractAddress, token_id: u128);
-}
-
-//
-// IERC721Metadata
-//
-
-#[starknet::interface]
-trait IERC721Metadata {
- fn name(self: @TState) -> felt252;
- fn symbol(self: @TState) -> felt252;
- fn token_uri(self: @TState, token_id: u128) -> felt252;
-}
-
-//
-// ERC721Receiver
-//
-
-#[starknet::interface]
-trait IERC721Receiver {
- fn on_erc721_received(
- self: @TState,
- operator: ContractAddress,
- from: ContractAddress,
- token_id: u128,
- data: Span
- ) -> felt252;
-}
-
-#[starknet::contract]
-mod ERC721 {
- use array::SpanTrait;
- use option::OptionTrait;
- use starknet::{get_caller_address, ContractAddress, get_contract_address};
- use zeroable::Zeroable;
- use super::IERC721;
- use debug::PrintTrait;
-
- #[storage]
- struct Storage {
- admin: ContractAddress,
- _name: felt252,
- _symbol: felt252,
- _owners: LegacyMap,
- _balances: LegacyMap,
- _token_approvals: LegacyMap,
- _operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>,
- _token_uri: LegacyMap,
- }
-
- #[event]
- #[derive(Drop, starknet::Event)]
- enum Event {
- Transfer: Transfer,
- Approval: Approval,
- ApprovalForAll: ApprovalForAll
- }
-
- #[derive(Drop, starknet::Event)]
- struct Transfer {
- from: ContractAddress,
- to: ContractAddress,
- token_id: u128
- }
-
- #[derive(Drop, starknet::Event)]
- struct Approval {
- owner: ContractAddress,
- approved: ContractAddress,
- token_id: u128
- }
-
- #[derive(Drop, starknet::Event)]
- struct ApprovalForAll {
- owner: ContractAddress,
- operator: ContractAddress,
- approved: bool
- }
-
- #[constructor]
- fn constructor(
- ref self: ContractState, name: felt252, symbol: felt252, admin: ContractAddress
- ) {
- self.initializer(name, symbol, admin);
- }
-
- //
- // External
- //
-
- #[external(v0)]
- impl ERC721Impl of IERC721 {
- fn initializer(
- ref self: ContractState, name_: felt252, symbol_: felt252, admin: ContractAddress
- ) {
- self._name.write(name_);
- self._symbol.write(symbol_);
- self.admin.write(admin);
- }
-
- fn balance_of(self: @ContractState, account: ContractAddress) -> u128 {
- assert(!account.is_zero(), 'ERC721: invalid account');
- self._balances.read(account)
- }
-
- fn owner_of(self: @ContractState, token_id: u128) -> ContractAddress {
- self._owner_of(token_id)
- }
-
- fn get_approved(self: @ContractState, token_id: u128) -> ContractAddress {
- assert(self._exists(token_id), 'ERC721: invalid token ID');
- self._token_approvals.read(token_id)
- }
-
- fn is_approved_for_all(
- self: @ContractState, owner: ContractAddress, operator: ContractAddress
- ) -> bool {
- self._operator_approvals.read((owner, operator))
- }
-
- fn approve(ref self: ContractState, to: ContractAddress, token_id: u128) {
- let owner = self._owner_of(token_id);
-
- let caller = get_caller_address();
- assert(
- owner == caller || ERC721Impl::is_approved_for_all(@self, owner, caller),
- 'ERC721: unauthorized caller'
- );
- self._approve(to, token_id);
- }
-
- fn set_approval_for_all(
- ref self: ContractState, operator: ContractAddress, approved: bool
- ) {
- self._set_approval_for_all(get_caller_address(), operator, approved)
- }
-
- fn transfer_from(
- ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u128
- ) {
- assert(
- self._is_approved_or_owner(get_caller_address(), token_id),
- 'ERC721: unauthorized caller'
- );
- self._transfer(from, to, token_id);
- }
-
- fn safe_transfer_from(
- ref self: ContractState,
- from: ContractAddress,
- to: ContractAddress,
- token_id: u128,
- data: Span
- ) {
- assert(
- self._is_approved_or_owner(get_caller_address(), token_id),
- 'ERC721: unauthorized caller'
- );
- self._safe_transfer(from, to, token_id, data);
- }
-
- fn mint(ref self: ContractState, to: ContractAddress, token_id: u128) {
- // Check if the contract is the admin
- assert(get_contract_address() == self.admin.read(), 'ERC721: unauthorized caller');
- self._mint(to, token_id);
- }
- }
-
-
- //
- // Internal
- //
-
- #[generate_trait]
- impl InternalImpl of InternalTrait {
- fn _owner_of(self: @ContractState, token_id: u128) -> ContractAddress {
- let owner = self._owners.read(token_id);
- match owner.is_zero() {
- bool::False(()) => owner,
- bool::True(()) => panic_with_felt252('ERC721: invalid token ID')
- }
- }
-
- fn _exists(self: @ContractState, token_id: u128) -> bool {
- !self._owners.read(token_id).is_zero()
- }
-
- fn _is_approved_or_owner(
- self: @ContractState, spender: ContractAddress, token_id: u128
- ) -> bool {
- let owner = self._owner_of(token_id);
- let is_approved_for_all = ERC721Impl::is_approved_for_all(self, owner, spender);
- owner == spender
- || is_approved_for_all
- || spender == ERC721Impl::get_approved(self, token_id)
- }
-
- fn _approve(ref self: ContractState, to: ContractAddress, token_id: u128) {
- let owner = self._owner_of(token_id);
- assert(owner != to, 'ERC721: approval to owner');
-
- self._token_approvals.write(token_id, to);
- self.emit(Approval { owner, approved: to, token_id });
- }
-
- fn _set_approval_for_all(
- ref self: ContractState,
- owner: ContractAddress,
- operator: ContractAddress,
- approved: bool
- ) {
- assert(owner != operator, 'ERC721: self approval');
- self._operator_approvals.write((owner, operator), approved);
- self.emit(ApprovalForAll { owner, operator, approved });
- }
-
- fn _mint(ref self: ContractState, to: ContractAddress, token_id: u128) {
- assert(!to.is_zero(), 'ERC721: invalid receiver');
- assert(!self._exists(token_id), 'ERC721: token already minted');
-
- self._balances.write(to, self._balances.read(to) + 1);
- self._owners.write(token_id, to);
-
- self.emit(Transfer { from: Zeroable::zero(), to, token_id });
- }
-
- fn _transfer(
- ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u128
- ) {
- assert(!to.is_zero(), 'ERC721: invalid receiver');
- let owner = self._owner_of(token_id);
- assert(from == owner, 'ERC721: wrong sender');
-
- // Implicit clear approvals, no need to emit an event
- self._token_approvals.write(token_id, Zeroable::zero());
-
- self._balances.write(from, self._balances.read(from) - 1);
- self._balances.write(to, self._balances.read(to) + 1);
- self._owners.write(token_id, to);
-
- self.emit(Transfer { from, to, token_id });
- }
-
- fn _burn(ref self: ContractState, token_id: u128) {
- let owner = self._owner_of(token_id);
-
- // Implicit clear approvals, no need to emit an event
- self._token_approvals.write(token_id, Zeroable::zero());
-
- self._balances.write(owner, self._balances.read(owner) - 1);
- self._owners.write(token_id, Zeroable::zero());
-
- self.emit(Transfer { from: owner, to: Zeroable::zero(), token_id });
- }
-
- fn _safe_mint(
- ref self: ContractState, to: ContractAddress, token_id: u128, data: Span
- ) {
- self._mint(to, token_id);
- }
-
- fn _safe_transfer(
- ref self: ContractState,
- from: ContractAddress,
- to: ContractAddress,
- token_id: u128,
- data: Span
- ) {
- self._transfer(from, to, token_id);
- }
-
- fn _set_token_uri(ref self: ContractState, token_id: u128, token_uri: felt252) {
- assert(self._exists(token_id), 'ERC721: invalid token ID');
- self._token_uri.write(token_id, token_uri)
- }
- }
-}
diff --git a/src/types/lockup.cairo b/src/types/lockup.cairo
index 68e512b..8c54a3a 100644
--- a/src/types/lockup.cairo
+++ b/src/types/lockup.cairo
@@ -9,31 +9,56 @@ use option::OptionTrait;
use traits::{TryInto, Into};
use zeroable::Zeroable;
use debug::PrintTrait;
+use tokei::types::lockup_linear::{Range, Broker, LockupLinearStream};
/// Represent Lockup amounts.
// Struct encapsulating the deposit, withdrawn, and refunded amounts, all denoted in units
/// of the asset's decimals.
-#[derive(Drop, Copy, starknet::Store, Serde)]
+#[derive(Drop, Copy, starknet::Store, Serde, PartialEq)]
struct LockupAmounts {
/// The initial amount deposited in the stream, net of fees.
- deposited: u128,
+ deposited: u256,
/// The cumulative amount withdrawn from the stream.
- withdrawn: u128,
+ withdrawn: u256,
/// The amount refunded to the sender. Unless the stream was canceled, this is always zero.
- refunded: u128,
+ refunded: u256,
}
/// Represent Create amounts.
/// Struct encapsulating the deposit amount, the protocol fee amount, and the broker fee amount,
/// all denoted in units of the asset's decimals.
-#[derive(Drop, Copy, starknet::Store, Serde)]
+#[derive(Drop, Copy, starknet::Store, Serde, PartialEq)]
struct CreateAmounts {
/// The amount to deposit in the stream.
- deposit: u128,
+ deposit: u256,
/// The protocol fee amount.
- protocol_fee: u128,
+ protocol_fee: u256,
/// The broker fee amount.
- broker_fee: u128,
+ broker_fee: u256,
+}
+
+/// Represent Stream status.
+/// Enum representing the status of a stream.
+#[derive(Serde, Copy, Drop, starknet::Store, PartialEq)]
+enum Status {
+ PENDING,
+ STREAMING,
+ SETTLED,
+ CANCELED,
+ DEPLETED,
+}
+
+/// Represent Status into felt252.
+impl StatusIntoFelt252 of Into {
+ fn into(self: Status) -> felt252 {
+ match self {
+ Status::PENDING(()) => 0,
+ Status::STREAMING(()) => 1,
+ Status::SETTLED(()) => 2,
+ Status::CANCELED(()) => 3,
+ Status::DEPLETED(()) => 4
+ }
+ }
}
/// Implementation of `Zeroable` trait for `CreateAmounts`.
@@ -57,6 +82,25 @@ impl CreateAmountsPrintTrait of PrintTrait {
let message = array![
'CreateAmounts: ', self.deposit.into(), self.protocol_fee.into(), self.broker_fee.into()
];
+ // message.print();
+ }
+}
+
+impl LockupLinearStreamPrintTrait of PrintTrait {
+ fn print(self: LockupLinearStream) {
+ let message = array![
+ 'LockupLinearStream: ',
+ self.sender.into(),
+ self.asset.into(),
+ self.start_time.into(),
+ self.cliff_time.into(),
+ self.end_time.into(),
+ self.is_cancelable.into(),
+ self.was_canceled.into(),
+ self.is_depleted.into(),
+ self.is_stream.into(),
+ self.is_transferable.into(),
+ ];
message.print();
}
}
diff --git a/src/types/lockup_linear.cairo b/src/types/lockup_linear.cairo
index 4162f08..86465b0 100644
--- a/src/types/lockup_linear.cairo
+++ b/src/types/lockup_linear.cairo
@@ -13,14 +13,20 @@ use starknet::ContractAddress;
use tokei::types::lockup::LockupAmounts;
/// Represent a Lockup Linear Stream.
-#[derive(Drop, starknet::Store, Serde)]
+#[derive(Copy, Drop, starknet::Store, Serde, PartialEq)]
struct LockupLinearStream {
+ /// The stream's ID.
+ stream_id: u64,
/// The address streaming the assets, with the ability to cancel the stream.
sender: ContractAddress,
/// The contract address of the ERC-20 asset used for streaming.
asset: ContractAddress,
+ /// The address receiving the streamed assets.
+ recipient: ContractAddress,
/// The Unix timestamp indicating the stream's start.
start_time: u64,
+ /// The Unix timestamp indicating the stream's cliff period's end.
+ cliff_time: u64,
/// The Unix timestamp indicating the stream's end.
end_time: u64,
/// Boolean indicating if the stream is cancelable.
@@ -29,13 +35,17 @@ struct LockupLinearStream {
was_canceled: bool,
/// Boolean indicating if the stream is depleted.
is_depleted: bool,
+ /// Boolean indicating if its a stream.
+ is_stream: bool,
+ /// Boolean indicating if the stream is transferable.
+ is_transferable: bool,
/// Struct containing the deposit, withdrawn, and refunded amounts, all denoted in units of the
/// asset's decimals.
amounts: LockupAmounts,
}
/// Represents a time range for linear lockups.
-#[derive(Drop, starknet::Store, Serde)]
+#[derive(Copy, Drop, starknet::Store, Serde, PartialEq)]
struct Range {
/// The timestamp for the stream's start.
start: u64,
@@ -46,10 +56,19 @@ struct Range {
}
/// Represents the broker parameters passed to the create functions. Both can be set to zero.
-#[derive(Drop, starknet::Store, Serde)]
+#[derive(Copy, Drop, starknet::Store, Serde)]
struct Broker {
/// The address receiving the broker's fee.
account: ContractAddress,
/// The broker's percentage fee from the total amount.
- fee: u128,
+ fee: u256,
+}
+
+/// Represents the durations for the cliff and total periods.
+#[derive(Copy, Drop, starknet::Store, Serde)]
+struct Durations {
+ /// The duration of the cliff period.
+ cliff: u64,
+ /// The total duration of the stream.
+ total: u64,
}
diff --git a/tests/test_lockup_linear.cairo b/tests/test_lockup_linear.cairo
deleted file mode 100644
index 3cc8065..0000000
--- a/tests/test_lockup_linear.cairo
+++ /dev/null
@@ -1,139 +0,0 @@
-//! Test file for `src/core/lockup_linear.cairo`.
-
-// *************************************************************************
-// IMPORTS
-// *************************************************************************
-
-// Core lib imports.
-use array::ArrayTrait;
-use result::ResultTrait;
-use option::OptionTrait;
-use traits::{TryInto, Into};
-use starknet::{
- ContractAddress, get_caller_address, Felt252TryIntoContractAddress, contract_address_const,
- ClassHash,
-};
-use debug::PrintTrait;
-
-// Starknet Foundry imports.
-use snforge_std::{
- declare, ContractClassTrait, start_prank, stop_prank, RevertedTransaction, CheatTarget,
- TxInfoMock,
-};
-
-use tokei::tokens::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
-
-
-// Local imports.
-use tokei::core::lockup_linear::{ITokeiLockupLinearDispatcher, ITokeiLockupLinearDispatcherTrait};
-use tokei::types::lockup_linear::{Range, Broker};
-use tokei::tokens::erc721::{IERC721SafeDispatcher, IERC721SafeDispatcherTrait};
-
-/// TODO: Implement actual test and change the name of this function.
-#[test]
-fn given_normal_conditions_when_create_with_range_then_expected_results() {
- // *********************************************************************************************
- // * SETUP *
- // *********************************************************************************************
- let caller_address = contract_address_const::<'caller'>();
-
- let (tokei) = setup(caller_address);
- let (token_dispatcher, token) = deploy_setup_erc20(
- 'Ethereum', 'ETH', 100000_u256, caller_address
- );
- start_prank(CheatTarget::One(token), caller_address);
- token_dispatcher.approve(tokei.contract_address, 1000_u256);
- stop_prank(CheatTarget::One(token));
-
- // *********************************************************************************************
- // * TEST LOGIC *
- // *********************************************************************************************
-
- // Define variables.
- let sender = caller_address;
- let recipient = contract_address_const::<'recipient'>();
- let total_amount = 1000;
- let asset = token;
- let cancelable = true;
- let start = 10;
- let cliff = 100;
- let end = 1000;
- let range = Range { start, cliff, end, };
- let broker_account = caller_address;
- let broker_fee = 0;
- let broker = Broker { account: broker_account, fee: broker_fee, };
-
- prepare_contracts(caller_address, tokei);
- // Actual test.
- let stream_id = tokei
- .create_with_range(sender, recipient, total_amount, asset, cancelable, range, broker,);
-
- // Assertions.
- assert(stream_id == 1, 'wrong stream id');
-
- let stream_nft = IERC721SafeDispatcher { contract_address: tokei.contract_address };
-
- // Check that the stream nft was minted to the recipient.
- assert(stream_nft.owner_of(stream_id.into()).unwrap() == recipient, 'wrong stream nft owner');
- assert(stream_nft.balance_of(recipient).unwrap() == 1, 'wrong stream nft balance');
-
- // *********************************************************************************************
- // * TEARDOWN *
- // *********************************************************************************************
- teardown(tokei);
-}
-
-/// Utility function to setup the test environment.
-fn setup(caller_address: ContractAddress) -> (ITokeiLockupLinearDispatcher,) {
- // Setup the contracts.
- let (tokei,) = setup_contracts(caller_address);
- // Prank the caller address.
- prepare_contracts(caller_address, tokei,);
- // Return the caller address and the contract interfaces.
- (tokei,)
-}
-
-// Utility function to prank the caller address
-fn prepare_contracts(caller_address: ContractAddress, tokei: ITokeiLockupLinearDispatcher,) {
- // Prank the caller address for calls to `TokeiLockupLinear` contract.
- start_prank(CheatTarget::One(tokei.contract_address), caller_address);
-}
-
-/// Utility function to teardown the test environment.
-fn teardown(tokei: ITokeiLockupLinearDispatcher,) {
- stop_prank(CheatTarget::One(tokei.contract_address));
-}
-
-/// Setup required contracts.
-fn setup_contracts(caller_address: ContractAddress) -> (ITokeiLockupLinearDispatcher,) {
- // Deploy the role store contract.
- let tokei_address = deploy_tokei(caller_address);
-
- // Create a role store dispatcher.
- let tokei = ITokeiLockupLinearDispatcher { contract_address: tokei_address };
-
- // Return the caller address and the contract interfaces.
- (tokei,)
-}
-
-
-/// Utility function to deploy a `TokeiLockupLinear` contract and return its address.
-fn deploy_tokei(initial_admin: ContractAddress) -> ContractAddress {
- let tokei_contract = declare('TokeiLockupLinear');
- let mut constructor_calldata = array![initial_admin.into()];
- tokei_contract.deploy(@constructor_calldata).unwrap()
-}
-
-fn deploy_setup_erc20(
- name: felt252, symbol: felt252, initial_supply: u256, recipient: ContractAddress
-) -> (IERC20Dispatcher, ContractAddress) {
- let token_contract = declare('ERC20');
- let mut calldata = array![name, symbol];
- Serde::serialize(@initial_supply, ref calldata);
- Serde::serialize(@recipient, ref calldata);
- let token_addr = token_contract.deploy(@calldata).unwrap();
- let token_dispatcher = IERC20Dispatcher { contract_address: token_addr };
-
- (token_dispatcher, token_addr)
-}
-