diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index 9c624f7..0000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -rustflags = ["-C", "link-args=-s"] \ No newline at end of file diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile deleted file mode 100644 index bc2641a..0000000 --- a/.gitpod.Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM gitpod/workspace-full - -ENV CARGO_HOME=/home/gitpod/.cargo - -RUN bash -cl "rustup toolchain install stable && rustup target add wasm32-unknown-unknown" - -RUN bash -c ". .nvm/nvm.sh \ - && nvm install v12 && nvm alias default v12" \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 4d043d4..0000000 --- a/.gitpod.yml +++ /dev/null @@ -1,7 +0,0 @@ -image: - file: .gitpod.Dockerfile -# List the start up tasks. You can start them in parallel in multiple terminals. See https://www.gitpod.io/docs/config-start-tasks/ -tasks: - - before: echo "nvm use default" >> ~/.bashrc && npm install -g near-cli --no-optional && nvm use default - init: yarn - command: clear && echo Hey! Check out examples of how to build a NEP-21 Fungible Token in Rust. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6b43dca --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fungible-token" +version = "1.0.0" +authors = ["Near Inc "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "5.5.0" +near-contract-standards = "5.5.0" + +[dev-dependencies] +near-sdk = { version = "5.5.0", features = ["unit-testing"] } +near-workspaces = { version = "0.14.1", features = ["unstable"] } +anyhow = "1.0" +tokio = { version = "1.41.0", features = ["full"] } diff --git a/README-Windows.md b/README-Windows.md deleted file mode 100644 index c73c261..0000000 --- a/README-Windows.md +++ /dev/null @@ -1,112 +0,0 @@ -Fungible Token (FT) -=================== - -Example implementation of a [Fungible Token] contract which uses [near-contract-standards] and [simulation] tests. This is a contract-only example. - -**Note**: this README is specific to Windows and this example. For development on OS X or Linux, please see [README.md](README.md). - - [Fungible Token]: https://nomicon.io/Standards/Tokens/FungibleTokenCore.html - [near-contract-standards]: https://github.com/near/near-sdk-rs/tree/master/near-contract-standards - [simulation]: https://github.com/near/near-sdk-rs/tree/master/near-sdk-sim - -Prerequisites -============= - -If you're using Gitpod, you can skip this step. - -1. Make sure Rust is installed per the prerequisites in [`near-sdk-rs`](https://github.com/near/near-sdk-rs#pre-requisites) -2. Ensure `near-cli` is installed by running `near --version`. If not installed, install with: `npm install --global near-cli` - -## Building - -To build run in CMD: -```bash -build.bat -``` - -Using this contract -=================== - -This smart contract will get deployed to your NEAR account. For this example, please create a new NEAR account. Because NEAR allows the ability to upgrade contracts on the same account, initialization functions must be cleared. If you'd like to run this example on a NEAR account that has had prior contracts deployed, please use the `near-cli` command `near delete`, and then recreate it in Wallet. To create (or recreate) an account, please follow the directions on [NEAR Wallet](https://wallet.near.org/). - -Switch to `mainnet`. You can skip this step to use `testnet` as a default network. - - set NEAR_ENV=mainnet - -In the project root, log in to your newly created account with `near-cli` by following the instructions after this command: - - near login - -To make this tutorial easier to copy/paste, we're going to set an environment variable for your account id. In the below command, replace `MY_ACCOUNT_NAME` with the account name you just logged in with, including the `.near`: - - set ID=MY_ACCOUNT_NAME - -You can tell if the environment variable is set correctly if your command line prints the account name after this command: - - echo %ID% - -Now we can deploy the compiled contract in this example to your account: - - near deploy --wasmFile res/fungible_token.wasm --accountId %ID% - -FT contract should be initialized before usage. You can read more about metadata at ['nomicon.io'](https://nomicon.io/Standards/FungibleToken/Metadata.html#reference-level-explanation). Modify the parameters and create a token: - - near call %ID% new "{\"owner_id\": \""%ID%"\", \"total_supply\": \"1000000000000000\", \"metadata\": { \"spec\": \"ft-1.0.0\", \"name\": \"Example Token Name\", \"symbol\": \"EXLT\", \"decimals\": 8 }}" --accountId %ID% - -Get metadata: - - near view %ID% ft_metadata - - -Transfer Example ---------------- - -Let's set up an account to transfer some tokens to. These account will be a sub-account of the NEAR account you logged in with. - - near create-account bob.%ID% --masterAccount %ID% --initialBalance 1 - -Add storage deposit for Bob's account: - - near call %ID% storage_deposit '' --accountId bob.%ID% --amount 0.00125 - -Check balance of Bob's account, it should be `0` for now: - - near view %ID% ft_balance_of "{\"account_id\": \""bob.%ID%"\"}" - -Transfer tokens to Bob from the contract that minted these fungible tokens, exactly 1 yoctoNEAR of deposit should be attached: - - near call %ID% ft_transfer "{\"receiver_id\": \""bob.%ID%"\", \"amount\": \"19\"}" --accountId %ID% --amount 0.000000000000000000000001 - - -Check the balance of Bob again with the command from before and it will now return `19`. - -## Testing - -As with many Rust libraries and contracts, there are tests in the main fungible token implementation at `ft/src/lib.rs`. - -Additionally, this project has [simulation] tests in `tests/sim`. Simulation tests allow testing cross-contract calls, which is crucial to ensuring that the `ft_transfer_call` function works properly. These simulation tests are the reason this project has the file structure it does. Note that the root project has a `Cargo.toml` which sets it up as a workspace. `ft` and `test-contract-defi` are both small & focused contract projects, the latter only existing for simulation tests. The root project imports `near-sdk-sim` and tests interaction between these contracts. - -You can run all these tests with one command: - -```bash -cargo test -``` - -If you want to run only simulation tests, you can use `cargo test simulate`, since all the simulation tests include "simulate" in their names. - - -## Notes - - - The maximum balance value is limited by U128 (`2**128 - 1`). - - JSON calls should pass U128 as a base-10 string. E.g. "100". - - This does not include escrow functionality, as `ft_transfer_call` provides a superior approach. An escrow system can, of course, be added as a separate contract or additional functionality within this contract. - -## No AssemblyScript? - -[near-contract-standards] is currently Rust-only. We strongly suggest using this library to create your own Fungible Token contract to ensure it works as expected. - -Someday NEAR core or community contributors may provide a similar library for AssemblyScript, at which point this example will be updated to include both a Rust and AssemblyScript version. - -## Contributing - -When making changes to the files in `ft` or `test-contract-defi`, remember to use `./build.sh` to compile all contracts and copy the output to the `res` folder. If you forget this, **the simulation tests will not use the latest versions**. diff --git a/README.md b/README.md index c4372c9..22c499b 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,58 @@ -Fungible Token (FT) -=================== +Fungible Token (FT) Example 👋 -Example implementation of a [Fungible Token] contract which uses [near-contract-standards] and [simulation] tests. This is a contract-only example. +[![](https://img.shields.io/badge/⋈%20Examples-Basics-green)](https://docs.near.org/tutorials/welcome) +[![](https://img.shields.io/badge/Contract-Rust-red)](contract-rs) - [Fungible Token]: https://nomicon.io/Standards/FungibleToken/Core - [near-contract-standards]: https://github.com/near/near-sdk-rs/tree/master/near-contract-standards - [simulation]: https://github.com/near/near-sdk-rs/tree/master/near-sdk-sim +This repository contains an example implementation of a [fungible token] contract in Rust which uses [near-contract-standards] and workspaces-rs tests. -Prerequisites -============= +[fungible token]: https://nomicon.io/Standards/FungibleToken/Core +[near-contract-standards]: https://github.com/near/near-sdk-rs/tree/master/near-contract-standards +[near-workspaces-rs]: https://github.com/near/near-workspaces-rs -If you're using Gitpod, you can skip this step. +
-1. Make sure Rust is installed per the prerequisites in [`near-sdk-rs`](https://github.com/near/near-sdk-rs#pre-requisites) -2. Ensure `near-cli` is installed by running `near --version`. If not installed, install with: `npm install -g near-cli` +## How to Build Locally? -## Building +Install [`cargo-near`](https://github.com/near/cargo-near) and run: -To build run: ```bash -./scripts/build.sh +cargo near build ``` -Using this contract -=================== - -### Quickest deploy - -You can build and deploy this smart contract to a development account. [Dev Accounts](https://docs.near.org/concepts/basics/account#dev-accounts) are auto-generated accounts to assist in developing and testing smart contracts. Please see the [Standard deploy](#standard-deploy) section for creating a more personalized account to deploy to. +## How to Test Locally? ```bash -near dev-deploy --wasmFile res/fungible_token.wasm --helperUrl https://near-contract-helper.onrender.com +cargo test ``` -Behind the scenes, this is creating an account and deploying a contract to it. On the console, notice a message like: +## How to Deploy? ->Done deploying to dev-1234567890123 - -In this instance, the account is `dev-1234567890123`. A file has been created containing a key pair to -the account, located at `neardev/dev-account`. To make the next few steps easier, we're going to set an -environment variable containing this development account id and use that when copy/pasting commands. -Run this command to the environment variable: +To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) and run: ```bash -source neardev/dev-account.env -``` - -You can tell if the environment variable is set correctly if your command line prints the account name after this command: -```bash -echo $CONTRACT_NAME -``` +# Create a new account +cargo near create-dev-account -The next command will initialize the contract using the `new` method: +# Deploy the contract on it +cargo near deploy -```bash -near call $CONTRACT_NAME new '{"owner_id": "'$CONTRACT_NAME'", "total_supply": "1000000000000000", "metadata": { "spec": "ft-1.0.0", "name": "Example Token Name", "symbol": "EXLT", "decimals": 8 }}' --accountId $CONTRACT_NAME +# Initialize the contract +near call new '{"owner_id": "", "total_supply": "1000000000000000", "metadata": { "spec": "ft-1.0.0", "name": "Example Token Name", "symbol": "EXLT", "decimals": 8 }}' --accountId ``` -To get the fungible token metadata: - +## Basic methods ```bash -near view $CONTRACT_NAME ft_metadata -``` - -### Standard deploy - -This smart contract will get deployed to your NEAR account. For this example, please create a new NEAR account. Because NEAR allows the ability to upgrade contracts on the same account, initialization functions must be cleared. If you'd like to run this example on a NEAR account that has had prior contracts deployed, please use the `near-cli` command `near delete`, and then recreate it in Wallet. To create (or recreate) an account, please follow the directions on [NEAR Wallet](https://wallet.near.org/). - -Switch to `mainnet`. You can skip this step to use `testnet` as a default network. - - export NEAR_ENV=mainnet - -In the project root, log in to your newly created account with `near-cli` by following the instructions after this command: - - near login - -To make this tutorial easier to copy/paste, we're going to set an environment variable for your account id. In the below command, replace `MY_ACCOUNT_NAME` with the account name you just logged in with, including the `.near`: - - ID=MY_ACCOUNT_NAME - -You can tell if the environment variable is set correctly if your command line prints the account name after this command: - - echo $ID - -Now we can deploy the compiled contract in this example to your account: - - near deploy --wasmFile res/fungible_token.wasm --accountId $ID - -FT contract should be initialized before usage. You can read more about metadata at ['nomicon.io'](https://nomicon.io/Standards/FungibleToken/Metadata.html#reference-level-explanation). Modify the parameters and create a token: - - near call $ID new '{"owner_id": "'$ID'", "total_supply": "1000000000000000", "metadata": { "spec": "ft-1.0.0", "name": "Example Token Name", "symbol": "EXLT", "decimals": 8 }}' --accountId $ID - -Get metadata: - - near view $ID ft_metadata +# View metadata +near view ft_metadata +# Make a storage deposit +near call storage_deposit '' --accountId --amount 0.00125 -Transfer Example ---------------- +# View balance +near view ft_balance_of '{"account_id": ""}' -Let's set up an account to transfer some tokens to. These account will be a sub-account of the NEAR account you logged in with. - - near create-account bob.$ID --masterAccount $ID --initialBalance 1 - -Add storage deposit for Bob's account: - - near call $ID storage_deposit '' --accountId bob.$ID --amount 0.00125 - - -Check balance of Bob's account, it should be `0` for now: - - near view $ID ft_balance_of '{"account_id": "'bob.$ID'"}' - -Transfer tokens to Bob from the contract that minted these fungible tokens, exactly 1 yoctoNEAR of deposit should be attached: - - near call $ID ft_transfer '{"receiver_id": "'bob.$ID'", "amount": "19"}' --accountId $ID --amount 0.000000000000000000000001 - - -Check the balance of Bob again with the command from before and it will now return `19`. - -## Testing - -As with many Rust libraries and contracts, there are tests in the main fungible token implementation at `ft/src/lib.rs`. - -Additionally, this project has [simulation] tests in `tests/sim`. Simulation tests allow testing cross-contract calls, which is crucial to ensuring that the `ft_transfer_call` function works properly. These simulation tests are the reason this project has the file structure it does. Note that the root project has a `Cargo.toml` which sets it up as a workspace. `ft` and `test-contract-defi` are both small & focused contract projects, the latter only existing for simulation tests. The root project imports `near-sdk-sim` and tests interaction between these contracts. - -You can run unit tests with the following command: - -```bash -cd ft && cargo test -- --nocapture --color=always -``` - -You can run integration tests with the following commands: -*Rust* -```bash -cd integration-tests/rs && cargo run --example integration-tests -``` -*TypeScript* -```bash -cd integration-tests/ts && yarn && yarn test +# Transfer tokens +near call ft_transfer '{"receiver_id": "", "amount": "19"}' --accountId --amount 0.000000000000000000000001 ``` ## Notes @@ -147,14 +61,13 @@ cd integration-tests/ts && yarn && yarn test - JSON calls should pass U128 as a base-10 string. E.g. "100". - This does not include escrow functionality, as `ft_transfer_call` provides a superior approach. An escrow system can, of course, be added as a separate contract or additional functionality within this contract. -## No AssemblyScript? - -[near-contract-standards] is currently Rust-only. We strongly suggest using this library to create your own Fungible Token contract to ensure it works as expected. - -Someday NEAR core or community contributors may provide a similar library for AssemblyScript, at which point this example will be updated to include both a Rust and AssemblyScript version. - -## Contributing - -When making changes to the files in `ft` or `test-contract-defi`, remember to use `./build.sh` to compile all contracts and copy the output to the `res` folder. If you forget this, **the simulation tests will not use the latest versions**. +## Useful Links -Note that if the `rust-toolchain` file in this repository changes, please make sure to update the `.gitpod.Dockerfile` to explicitly specify using that as default as well. +- [cargo-near](https://github.com/near/cargo-near) - NEAR smart contract development toolkit for Rust +- [near CLI](https://near.cli.rs) - Iteract with NEAR blockchain from command line +- [NEAR Rust SDK Documentation](https://docs.near.org/sdk/rust/introduction) +- [NEAR Documentation](https://docs.near.org) +- [NEAR StackOverflow](https://stackoverflow.com/questions/tagged/nearprotocol) +- [NEAR Discord](https://near.chat) +- [NEAR Telegram Developers Community Group](https://t.me/neardev) +- NEAR DevHub: [Telegram](https://t.me/neardevhub), [Twitter](https://twitter.com/neardevhub) \ No newline at end of file diff --git a/ft/Cargo.toml b/ft/Cargo.toml deleted file mode 100644 index d62bce4..0000000 --- a/ft/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "fungible-token" -version = "1.0.0" -authors = ["Near Inc "] -edition = "2018" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -near-sdk = "4.0.0" -near-contract-standards = "4.0.0" - -# This can be removed when near-sdk is updated -# Unfortuantely, this crate was yanked by the author and this is needed -[patch.crates-io] -parity-secp256k1 = { git = 'https://github.com/paritytech/rust-secp256k1.git' } \ No newline at end of file diff --git a/integration-tests/rs/Cargo.toml b/integration-tests/rs/Cargo.toml deleted file mode 100644 index f752878..0000000 --- a/integration-tests/rs/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "fungible-token-integration-tests" -version = "1.0.0" -publish = false -edition = "2018" - -[dev-dependencies] -near-sdk = "4.0.0-pre.7" -anyhow = "1.0" -borsh = "0.9" -maplit = "1.0" -near-units = "0.2.0" -# arbitrary_precision enabled for u128 types that workspaces requires for Balance types -serde_json = { version = "1.0", features = ["arbitrary_precision"] } -tokio = { version = "1.18.1", features = ["full"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } -workspaces = "0.4.0" -pkg-config = "0.3.1" - -[[example]] -name = "integration-tests" -path = "src/tests.rs" - -# This can be removed when near-sdk is updated -# Unfortuantely, this crate was yanked by the author and this is needed -[patch.crates-io] -parity-secp256k1 = { git = 'https://github.com/paritytech/rust-secp256k1.git' } \ No newline at end of file diff --git a/integration-tests/rs/src/tests.rs b/integration-tests/rs/src/tests.rs deleted file mode 100644 index c677c92..0000000 --- a/integration-tests/rs/src/tests.rs +++ /dev/null @@ -1,507 +0,0 @@ -use near_sdk::json_types::U128; -use near_units::{parse_gas, parse_near}; -use serde_json::json; -use workspaces::prelude::*; -use workspaces::result::CallExecutionDetails; -use workspaces::{network::Sandbox, Account, Contract, Worker}; - -const DEFI_WASM_FILEPATH: &str = "../../res/defi.wasm"; -const FT_WASM_FILEPATH: &str = "../../res/fungible_token.wasm"; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // initiate environemnt - let worker = workspaces::sandbox().await?; - - // deploy contracts - let defi_wasm = std::fs::read(DEFI_WASM_FILEPATH)?; - let defi_contract = worker.dev_deploy(&defi_wasm).await?; - let ft_wasm = std::fs::read(FT_WASM_FILEPATH)?; - let ft_contract = worker.dev_deploy(&ft_wasm).await?; - - // create accounts - let owner = worker.root_account().unwrap(); - let alice = owner - .create_subaccount(&worker, "alice") - .initial_balance(parse_near!("30 N")) - .transact() - .await? - .into_result()?; - let bob = owner - .create_subaccount(&worker, "bob") - .initial_balance(parse_near!("30 N")) - .transact() - .await? - .into_result()?; - let charlie = owner - .create_subaccount(&worker, "charlie") - .initial_balance(parse_near!("30 N")) - .transact() - .await? - .into_result()?; - let dave = owner - .create_subaccount(&worker, "dave") - .initial_balance(parse_near!("30 N")) - .transact() - .await? - .into_result()?; - - // Initialize contracts - ft_contract - .call(&worker, "new_default_meta") - .args_json(serde_json::json!({ - "owner_id": owner.id(), - "total_supply": parse_near!("1,000,000,000 N").to_string(), - }))? - .transact() - .await?; - defi_contract - .call(&worker, "new") - .args_json(serde_json::json!({ - "fungible_token_account_id": ft_contract.id() - }))? - .transact() - .await?; - defi_contract - .as_account() - .call(&worker, ft_contract.id(), "storage_deposit") - .args_json(serde_json::json!({ - "account_id": defi_contract.id() - }))? - .deposit(parse_near!("0.008 N")) - .transact() - .await?; - - // begin tests - test_total_supply(&owner, &ft_contract, &worker).await?; - test_simple_transfer(&owner, &alice, &ft_contract, &worker).await?; - test_can_close_empty_balance_account(&bob, &ft_contract, &worker).await?; - test_close_account_non_empty_balance(&alice, &ft_contract, &worker).await?; - test_close_account_force_non_empty_balance(&alice, &ft_contract, &worker).await?; - test_transfer_call_with_burned_amount(&owner, &charlie, &ft_contract, &defi_contract, &worker) - .await?; - test_simulate_transfer_call_with_immediate_return_and_no_refund( - &owner, - &ft_contract, - &defi_contract, - &worker, - ) - .await?; - test_transfer_call_when_called_contract_not_registered_with_ft( - &owner, - &dave, - &ft_contract, - &worker, - ) - .await?; - test_transfer_call_promise_panics_for_a_full_refund(&owner, &alice, &ft_contract, &worker) - .await?; - Ok(()) -} - -async fn test_total_supply( - owner: &Account, - contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let initial_balance = U128::from(parse_near!("1,000,000,000 N")); - let res: U128 = owner - .call(&worker, contract.id(), "ft_total_supply") - .args_json(json!({}))? - .transact() - .await? - .json()?; - assert_eq!(res, initial_balance); - println!(" Passed ✅ test_total_supply"); - Ok(()) -} - -async fn test_simple_transfer( - owner: &Account, - user: &Account, - contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let transfer_amount = U128::from(parse_near!("1,000 N")); - - // register user - user.call(&worker, contract.id(), "storage_deposit") - .args_json(serde_json::json!({ - "account_id": user.id() - }))? - .deposit(parse_near!("0.008 N")) - .transact() - .await?; - - // transfer ft - owner - .call(&worker, contract.id(), "ft_transfer") - .args_json(serde_json::json!({ - "receiver_id": user.id(), - "amount": transfer_amount - }))? - .deposit(1) - .transact() - .await?; - - let root_balance: U128 = owner - .call(&worker, contract.id(), "ft_balance_of") - .args_json(serde_json::json!({ - "account_id": owner.id() - }))? - .transact() - .await? - .json()?; - - let alice_balance: U128 = owner - .call(&worker, contract.id(), "ft_balance_of") - .args_json(serde_json::json!({ - "account_id": user.id() - }))? - .transact() - .await? - .json()?; - - assert_eq!(root_balance, U128::from(parse_near!("999,999,000 N"))); - assert_eq!(alice_balance, transfer_amount); - - println!(" Passed ✅ test_simple_transfer"); - Ok(()) -} - -async fn test_can_close_empty_balance_account( - user: &Account, - contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - // register user - user.call(&worker, contract.id(), "storage_deposit") - .args_json(serde_json::json!({ - "account_id": user.id() - }))? - .deposit(parse_near!("0.008 N")) - .transact() - .await?; - - let result: bool = user - .call(&worker, contract.id(), "storage_unregister") - .args_json(serde_json::json!({}))? - .deposit(1) - .transact() - .await? - .json()?; - - assert_eq!(result, true); - println!(" Passed ✅ test_can_close_empty_balance_account"); - Ok(()) -} - -async fn test_close_account_non_empty_balance( - user_with_funds: &Account, - contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - match user_with_funds - .call(&worker, contract.id(), "storage_unregister") - .args_json(serde_json::json!({}))? - .deposit(1) - .transact() - .await - { - Ok(_result) => { - panic!("storage_unregister worked despite account being funded") - } - Err(e) => { - let e_string = e.to_string(); - if !e_string - .contains("Can't unregister the account with the positive balance without force") - { - panic!("storage_unregister with balance displays unexpected error message") - } - println!(" Passed ✅ test_close_account_non_empty_balance"); - } - } - Ok(()) -} - -async fn test_close_account_force_non_empty_balance( - user_with_funds: &Account, - contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let result: CallExecutionDetails = user_with_funds - .call(&worker, contract.id(), "storage_unregister") - .args_json(serde_json::json!({"force": true }))? - .deposit(1) - .transact() - .await?; - - assert_eq!(true, result.is_success()); - assert_eq!( - result.logs()[0], - format!( - "Closed @{} with {}", - user_with_funds.id(), - parse_near!("1,000 N") // alice balance from above transfer_amount - ) - ); - println!(" Passed ✅ test_close_account_force_non_empty_balance"); - Ok(()) -} - -async fn test_transfer_call_with_burned_amount( - owner: &Account, - user: &Account, - ft_contract: &Contract, - defi_contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let transfer_amount_str = parse_near!("1,000,000 N").to_string(); - let ftc_amount_str = parse_near!("1,000 N").to_string(); - - // register user - owner - .call(&worker, ft_contract.id(), "storage_deposit") - .args_json(serde_json::json!({ - "account_id": user.id() - }))? - .deposit(parse_near!("0.008 N")) - .transact() - .await?; - - // transfer ft - owner - .call(&worker, ft_contract.id(), "ft_transfer") - .args_json(serde_json::json!({ - "receiver_id": user.id(), - "amount": transfer_amount_str - }))? - .deposit(1) - .transact() - .await?; - - user.call(&worker, ft_contract.id(), "ft_transfer_call") - .args_json(serde_json::json!({ - "receiver_id": defi_contract.id(), - "amount": ftc_amount_str, - "msg": "0", - }))? - .deposit(1) - .gas(parse_gas!("200 Tgas") as u64) - .transact() - .await?; - - let storage_result: CallExecutionDetails = user - .call(&worker, ft_contract.id(), "storage_unregister") - .args_json(serde_json::json!({"force": true }))? - .deposit(1) - .transact() - .await?; - - // assert new state - assert_eq!( - storage_result.logs()[0], - format!( - "Closed @{} with {}", - user.id(), - parse_near!("999,000 N") // balance after defi ft transfer - ) - ); - - let total_supply: U128 = owner - .call(&worker, ft_contract.id(), "ft_total_supply") - .args_json(json!({}))? - .transact() - .await? - .json()?; - assert_eq!(total_supply, U128::from(parse_near!("999,000,000 N"))); - - let defi_balance: U128 = owner - .call(&worker, ft_contract.id(), "ft_total_supply") - .args_json(json!({"account_id": defi_contract.id()}))? - .transact() - .await? - .json()?; - assert_eq!(defi_balance, U128::from(parse_near!("999,000,000 N"))); - - println!(" Passed ✅ test_transfer_call_with_burned_amount"); - Ok(()) -} - -async fn test_simulate_transfer_call_with_immediate_return_and_no_refund( - owner: &Account, - ft_contract: &Contract, - defi_contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let amount: u128 = parse_near!("100,000,000 N"); - let amount_str = amount.to_string(); - let owner_before_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": owner.id()}))? - .transact() - .await? - .json()?; - let defi_before_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": defi_contract.id()}))? - .transact() - .await? - .json()?; - - owner - .call(&worker, ft_contract.id(), "ft_transfer_call") - .args_json(serde_json::json!({ - "receiver_id": defi_contract.id(), - "amount": amount_str, - "msg": "take-my-money" - }))? - .deposit(1) - .gas(parse_gas!("200 Tgas") as u64) - .transact() - .await?; - - let owner_after_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": owner.id()}))? - .transact() - .await? - .json()?; - let defi_after_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": defi_contract.id()}))? - .transact() - .await? - .json()?; - - assert_eq!(owner_before_balance.0 - amount, owner_after_balance.0); - assert_eq!(defi_before_balance.0 + amount, defi_after_balance.0); - println!(" Passed ✅ test_simulate_transfer_call_with_immediate_return_and_no_refund"); - Ok(()) -} - -async fn test_transfer_call_when_called_contract_not_registered_with_ft( - owner: &Account, - user: &Account, - ft_contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let amount = parse_near!("10 N"); - let amount_str = amount.to_string(); - let owner_before_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": owner.id()}))? - .transact() - .await? - .json()?; - let user_before_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": user.id()}))? - .transact() - .await? - .json()?; - - match owner - .call(&worker, ft_contract.id(), "ft_transfer_call") - .args_json(serde_json::json!({ - "receiver_id": user.id(), - "amount": amount_str, - "msg": "take-my-money", - }))? - .deposit(1) - .gas(parse_gas!("200 Tgas") as u64) - .transact() - .await - { - Ok(res) => { - panic!("Was able to transfer FT to an unregistered account"); - } - Err(err) => { - let owner_after_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": owner.id()}))? - .transact() - .await? - .json()?; - let user_after_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": user.id()}))? - .transact() - .await? - .json()?; - assert_eq!(user_before_balance, user_after_balance); - assert_eq!(owner_before_balance, owner_after_balance); - println!( - " Passed ✅ test_transfer_call_when_called_contract_not_registered_with_ft" - ); - } - } - Ok(()) -} - -async fn test_transfer_call_promise_panics_for_a_full_refund( - owner: &Account, - user: &Account, - ft_contract: &Contract, - worker: &Worker, -) -> anyhow::Result<()> { - let amount = parse_near!("10 N"); - - // register user - owner - .call(&worker, ft_contract.id(), "storage_deposit") - .args_json(serde_json::json!({ - "account_id": user.id() - }))? - .deposit(parse_near!("0.008 N")) - .transact() - .await?; - - let owner_before_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": owner.id()}))? - .transact() - .await? - .json()?; - let user_before_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": user.id()}))? - .transact() - .await? - .json()?; - - match owner - .call(&worker, ft_contract.id(), "ft_transfer_call") - .args_json(serde_json::json!({ - "receiver_id": user.id(), - "amount": amount, - "msg": "no parsey as integer big panic oh no", - }))? - .deposit(1) - .gas(parse_gas!("200 Tgas") as u64) - .transact() - .await - { - Ok(res) => { - panic!("Did not expect for trx to accept invalid paramenter data types") - } - Err(err) => { - let owner_after_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": owner.id()}))? - .transact() - .await? - .json()?; - let user_after_balance: U128 = ft_contract - .call(&worker, "ft_balance_of") - .args_json(json!({"account_id": user.id()}))? - .transact() - .await? - .json()?; - assert_eq!(owner_before_balance, owner_after_balance); - assert_eq!(user_before_balance, user_after_balance); - println!(" Passed ✅ test_transfer_call_promise_panics_for_a_full_refund"); - } - } - Ok(()) -} diff --git a/integration-tests/ts/ava.config.cjs b/integration-tests/ts/ava.config.cjs deleted file mode 100644 index c488d9d..0000000 --- a/integration-tests/ts/ava.config.cjs +++ /dev/null @@ -1,9 +0,0 @@ -require("util").inspect.defaultOptions.depth = 5; // Increase AVA's printing depth - -module.exports = { - timeout: "300000", - files: ["**/*.ava.ts"], - failWithoutAssertions: false, - extensions: ["ts"], - require: ["ts-node/register"], -}; diff --git a/integration-tests/ts/package.json b/integration-tests/ts/package.json deleted file mode 100644 index 3f8abf3..0000000 --- a/integration-tests/ts/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "fungible-token-integration-tests-ts", - "version": "0.1.0", - "license": "(MIT AND Apache-2.0)", - "scripts": { - "test": "ava --verbose" - }, - "devDependencies": { - "ava": "^4.2.0", - "near-workspaces": "^3.1.0", - "typescript": "^4.6.4", - "ts-node": "^10.8.0", - "@types/bn.js": "^5.1.0" - }, - "dependencies": {} -} diff --git a/integration-tests/ts/src/main.ava.ts b/integration-tests/ts/src/main.ava.ts deleted file mode 100644 index 9e6d83c..0000000 --- a/integration-tests/ts/src/main.ava.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * This tests the behavior of the standard FT contract at - * https://github.com/near/near-sdk-rs/tree/master/examples/fungible-token - * - * Some advanced features of near-workspaces this shows off: - * - * - Cross-Contract Calls: the "defi" contract implements basic features that - * might be used by a marketplace contract. You can see its source code at the - * near-sdk-rs link above. Several FT methods make cross-contract calls, and - * these are tested below using this "defi" contract. - * - * - Complex transactions: to exercise certain edge cases of the FT standard, - * tests below initiate chains of transactions using near-workspaces's transaction - * builder. Search for `batch` below. - */ -import { Worker, NearAccount, captureError, NEAR, BN } from 'near-workspaces'; -import anyTest, { TestFn } from 'ava'; - -const STORAGE_BYTE_COST = '1.5 mN'; -const INITIAL_SUPPLY = "10000"; - -async function registerUser(ft: NearAccount, user: NearAccount) { - await user.call( - ft, - 'storage_deposit', - { account_id: user }, - // Deposit pulled from ported sim test - { attachedDeposit: STORAGE_BYTE_COST }, - ); -} - -async function ft_balance_of(ft: NearAccount, user: NearAccount): Promise { - return new BN(await ft.view('ft_balance_of', { account_id: user })); -} - -const test = anyTest as TestFn<{ - worker: Worker; - accounts: Record; -}>; - -test.beforeEach(async t => { - const worker = await Worker.init(); - const root = worker.rootAccount; - const ft = await root.devDeploy( - "../../res/fungible_token.wasm", - { - initialBalance: NEAR.parse('100 N').toJSON(), - method: "new_default_meta", - args: { - owner_id: root, - total_supply: INITIAL_SUPPLY, - } - }, - ); - const defi = await root.devDeploy( - '../../res/defi.wasm', - { - initialBalance: NEAR.parse('100 N').toJSON(), - method: "new", - args: { fungible_token_account_id: ft } - }, - ); - - const ali = await root.createSubAccount('ali', { initialBalance: NEAR.parse('100 N').toJSON() }); - - t.context.worker = worker; - t.context.accounts = { root, ft, defi, ali }; -}); - -test.afterEach(async t => { - await t.context.worker.tearDown().catch(error => { - console.log('Failed to tear down the worker:', error); - }); -}); - -test('Total supply', async t => { - const { ft } = t.context.accounts; - const totalSupply: string = await ft.view('ft_total_supply'); - t.is(totalSupply, INITIAL_SUPPLY); -}); - -test('Simple transfer', async t => { - const { ft, ali, root } = t.context.accounts; - const initialAmount = new BN(INITIAL_SUPPLY); - const transferAmount = new BN('100'); - - // Register by prepaying for storage. - await registerUser(ft, ali); - - await root.call( - ft, - 'ft_transfer', - { - receiver_id: ali, - amount: transferAmount, - }, - { attachedDeposit: '1' }, - ); - - const rootBalance = await ft_balance_of(ft, root); - const aliBalance = await ft_balance_of(ft, ali); - - t.deepEqual(new BN(rootBalance), initialAmount.sub(transferAmount)); - t.deepEqual(new BN(aliBalance), transferAmount); -}); - -test('Can close empty balance account', async t => { - const { ft, ali } = t.context.accounts; - - await registerUser(ft, ali); - - const result = await ali.call( - ft, - 'storage_unregister', - {}, - { attachedDeposit: '1' }, - ); - - t.is(result, true); -}); - -test('Can force close non-empty balance account', async t => { - const { ft, root } = t.context.accounts; - - const errorString = await captureError(async () => - root.call(ft, 'storage_unregister', {}, { attachedDeposit: '1' })); - t.regex(errorString, /Can't unregister the account with the positive balance without force/); - - const result = await root.callRaw( - ft, - 'storage_unregister', - { force: true }, - { attachedDeposit: '1' }, - ); - - t.is(result.logs[0], - `Closed @${root.accountId} with ${INITIAL_SUPPLY}`, - ); -}); - -test('Transfer call with burned amount', async t => { - const { ft, defi, root } = t.context.accounts; - - const initialAmount = new BN(10_000); - const transferAmount = new BN(100); - const burnAmount = new BN(10); - - await registerUser(ft, defi); - const result = await root - .batch(ft) - .functionCall( - 'ft_transfer_call', - { - receiver_id: defi, - amount: transferAmount, - msg: burnAmount, - }, - { attachedDeposit: '1', gas: '150 Tgas' }, - ) - .functionCall( - 'storage_unregister', - { force: true }, - { attachedDeposit: '1', gas: '150 Tgas' }, - ) - .transact(); - - t.true(result.logs.includes( - `Closed @${root.accountId} with ${(initialAmount.sub(transferAmount)).toString()}`, - )); - - t.is(result.parseResult(), true); - - t.true(result.logs.includes( - 'The account of the sender was deleted', - )); - - t.true(result.logs.includes( - `Account @${root.accountId} burned ${burnAmount.toString()}`, - )); - - // Help: this index is diff from sim, we have 10 len when they have 4 - const callbackOutcome = result.receipts_outcomes[5]; - t.is(callbackOutcome.parseResult(), transferAmount.toString()); - const expectedAmount = transferAmount.sub(burnAmount); - const totalSupply: string = await ft.view('ft_total_supply'); - t.is(totalSupply, expectedAmount.toString()); - const defiBalance = await ft_balance_of(ft, defi); - t.deepEqual(defiBalance, expectedAmount); -}); - -test('Transfer call immediate return no refund', async t => { - const { ft, defi, root } = t.context.accounts; - const initialAmount = new BN(10_000); - const transferAmount = new BN(100); - - await registerUser(ft, defi); - - await root.call( - ft, - 'ft_transfer_call', - { - receiver_id: defi, - amount: transferAmount, - memo: null, - msg: 'take-my-money', - }, - { attachedDeposit: '1', gas: '150 Tgas' }, - ); - - const rootBalance = await ft_balance_of(ft, root); - const defiBalance = await ft_balance_of(ft, defi); - - t.deepEqual(rootBalance, initialAmount.sub(transferAmount)); - t.deepEqual(defiBalance, transferAmount); -}); - -test('Transfer call promise panics for a full refund', async t => { - const { ft, defi, root } = t.context.accounts; - const initialAmount = new BN(10_000); - const transferAmount = new BN(100); - - await registerUser(ft, defi); - - const result = await root.callRaw( - ft, - 'ft_transfer_call', - { - receiver_id: defi, - amount: transferAmount, - memo: null, - msg: 'this won\'t parse as an integer', - }, - { attachedDeposit: '1', gas: '150 Tgas' }, - ); - - t.regex(result.receiptFailureMessages.join('\n'), /ParseIntError/); - - const rootBalance = await ft_balance_of(ft, root); - const defiBalance = await ft_balance_of(ft, defi); - - t.deepEqual(rootBalance, initialAmount); - t.assert(defiBalance.isZero(), `Expected zero got ${defiBalance.toJSON()}`); -}); diff --git a/res/defi.wasm b/res/defi.wasm deleted file mode 100755 index ecf10cd..0000000 Binary files a/res/defi.wasm and /dev/null differ diff --git a/res/fungible_token.wasm b/res/fungible_token.wasm deleted file mode 100755 index 40045b5..0000000 Binary files a/res/fungible_token.wasm and /dev/null differ diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 2a35f02..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -use_small_heuristics = "Max" diff --git a/scripts/build.bat b/scripts/build.bat deleted file mode 100644 index 2208979..0000000 --- a/scripts/build.bat +++ /dev/null @@ -1,7 +0,0 @@ -@echo off - -title FT build -cd .. -cargo build --all --target wasm32-unknown-unknown --release -xcopy %CD%\target\wasm32-unknown-unknown\release\*.wasm %CD%\res /Y -pause \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index 356b188..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -e -cd "`dirname $0`"/../ft -cargo build --all --target wasm32-unknown-unknown --release -cd .. -cp ft/target/wasm32-unknown-unknown/release/*.wasm ./res/ diff --git a/ft/src/lib.rs b/src/lib.rs similarity index 62% rename from ft/src/lib.rs rename to src/lib.rs index a52117e..27742ff 100644 --- a/ft/src/lib.rs +++ b/src/lib.rs @@ -18,14 +18,21 @@ NOTES: use near_contract_standards::fungible_token::metadata::{ FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC, }; -use near_contract_standards::fungible_token::FungibleToken; -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_contract_standards::fungible_token::{ + FungibleToken, FungibleTokenCore, FungibleTokenResolver, +}; +use near_contract_standards::storage_management::{ + StorageBalance, StorageBalanceBounds, StorageManagement, +}; +use near_sdk::borsh::BorshSerialize; use near_sdk::collections::LazyOption; use near_sdk::json_types::U128; -use near_sdk::{env, log, near_bindgen, AccountId, Balance, PanicOnDefault, PromiseOrValue}; +use near_sdk::{ + env, log, near, require, AccountId, BorshStorageKey, NearToken, PanicOnDefault, PromiseOrValue, +}; -#[near_bindgen] -#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +#[derive(PanicOnDefault)] +#[near(contract_state)] pub struct Contract { token: FungibleToken, metadata: LazyOption, @@ -33,7 +40,14 @@ pub struct Contract { const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 288 288'%3E%3Cg id='l' data-name='l'%3E%3Cpath d='M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z'/%3E%3C/g%3E%3C/svg%3E"; -#[near_bindgen] +#[derive(BorshSerialize, BorshStorageKey)] +#[borsh(crate = "near_sdk::borsh")] +enum StorageKey { + FungibleToken, + Metadata, +} + +#[near] impl Contract { /// Initializes the contract with the given total supply owned by the given `owner_id` with /// default metadata (for example purposes only). @@ -57,41 +71,110 @@ impl Contract { /// Initializes the contract with the given total supply owned by the given `owner_id` with /// the given fungible token metadata. #[init] - pub fn new( - owner_id: AccountId, - total_supply: U128, - metadata: FungibleTokenMetadata, - ) -> Self { - assert!(!env::state_exists(), "Already initialized"); + pub fn new(owner_id: AccountId, total_supply: U128, metadata: FungibleTokenMetadata) -> Self { + require!(!env::state_exists(), "Already initialized"); metadata.assert_valid(); let mut this = Self { - token: FungibleToken::new(b"a".to_vec()), - metadata: LazyOption::new(b"m".to_vec(), Some(&metadata)), + token: FungibleToken::new(StorageKey::FungibleToken), + metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)), }; this.token.internal_register_account(&owner_id); this.token.internal_deposit(&owner_id, total_supply.into()); + near_contract_standards::fungible_token::events::FtMint { owner_id: &owner_id, - amount: &total_supply, - memo: Some("Initial tokens supply is minted"), + amount: total_supply, + memo: Some("new tokens are minted"), } .emit(); + this } +} + +#[near] +impl FungibleTokenCore for Contract { + #[payable] + fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { + self.token.ft_transfer(receiver_id, amount, memo) + } - fn on_account_closed(&mut self, account_id: AccountId, balance: Balance) { - log!("Closed @{} with {}", account_id, balance); + #[payable] + fn ft_transfer_call( + &mut self, + receiver_id: AccountId, + amount: U128, + memo: Option, + msg: String, + ) -> PromiseOrValue { + self.token.ft_transfer_call(receiver_id, amount, memo, msg) } - fn on_tokens_burned(&mut self, account_id: AccountId, amount: Balance) { - log!("Account @{} burned {}", account_id, amount); + fn ft_total_supply(&self) -> U128 { + self.token.ft_total_supply() + } + + fn ft_balance_of(&self, account_id: AccountId) -> U128 { + self.token.ft_balance_of(account_id) } } -near_contract_standards::impl_fungible_token_core!(Contract, token, on_tokens_burned); -near_contract_standards::impl_fungible_token_storage!(Contract, token, on_account_closed); +#[near] +impl FungibleTokenResolver for Contract { + #[private] + fn ft_resolve_transfer( + &mut self, + sender_id: AccountId, + receiver_id: AccountId, + amount: U128, + ) -> U128 { + let (used_amount, burned_amount) = + self.token + .internal_ft_resolve_transfer(&sender_id, receiver_id, amount); + if burned_amount > 0 { + log!("Account @{} burned {}", sender_id, burned_amount); + } + used_amount.into() + } +} + +#[near] +impl StorageManagement for Contract { + #[payable] + fn storage_deposit( + &mut self, + account_id: Option, + registration_only: Option, + ) -> StorageBalance { + self.token.storage_deposit(account_id, registration_only) + } + + #[payable] + fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { + self.token.storage_withdraw(amount) + } + + #[payable] + fn storage_unregister(&mut self, force: Option) -> bool { + #[allow(unused_variables)] + if let Some((account_id, balance)) = self.token.internal_storage_unregister(force) { + log!("Closed @{} with {}", account_id, balance); + true + } else { + false + } + } + + fn storage_balance_bounds(&self) -> StorageBalanceBounds { + self.token.storage_balance_bounds() + } + + fn storage_balance_of(&self, account_id: AccountId) -> Option { + self.token.storage_balance_of(account_id) + } +} -#[near_bindgen] +#[near] impl FungibleTokenMetadataProvider for Contract { fn ft_metadata(&self) -> FungibleTokenMetadata { self.metadata.get().unwrap() @@ -100,9 +183,9 @@ impl FungibleTokenMetadataProvider for Contract { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { + use near_contract_standards::fungible_token::Balance; use near_sdk::test_utils::{accounts, VMContextBuilder}; - use near_sdk::MockedBlockchain; - use near_sdk::{testing_env, Balance}; + use near_sdk::testing_env; use super::*; @@ -150,7 +233,7 @@ mod tests { testing_env!(context .storage_usage(env::storage_usage()) - .attached_deposit(1) + .attached_deposit(NearToken::from_yoctonear(1)) .predecessor_account_id(accounts(2)) .build()); let transfer_amount = TOTAL_SUPPLY / 3; @@ -160,9 +243,12 @@ mod tests { .storage_usage(env::storage_usage()) .account_balance(env::account_balance()) .is_view(true) - .attached_deposit(0) + .attached_deposit(NearToken::from_near(0)) .build()); - assert_eq!(contract.ft_balance_of(accounts(2)).0, (TOTAL_SUPPLY - transfer_amount)); + assert_eq!( + contract.ft_balance_of(accounts(2)).0, + (TOTAL_SUPPLY - transfer_amount) + ); assert_eq!(contract.ft_balance_of(accounts(1)).0, transfer_amount); } } diff --git a/test-contract-defi/Cargo.toml b/test-contract-defi/Cargo.toml deleted file mode 100644 index 674e6fb..0000000 --- a/test-contract-defi/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "defi" -version = "0.0.1" -authors = ["Near Inc "] -edition = "2018" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -near-sdk = "4.0.0" -near-contract-standards = "4.0.0" - -# This can be removed when near-sdk is updated -# Unfortuantely, this crate was yanked by the author and this is needed -[patch.crates-io] -parity-secp256k1 = { git = 'https://github.com/paritytech/rust-secp256k1.git' } \ No newline at end of file diff --git a/tests/contracts/defi/Cargo.toml b/tests/contracts/defi/Cargo.toml new file mode 100644 index 0000000..356c0a5 --- /dev/null +++ b/tests/contracts/defi/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "defi" +version = "0.0.1" +authors = ["Near Inc "] +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +near-sdk = "5.5.0" +near-contract-standards = "5.5.0" + +[dev-dependencies] +near-sdk = { version = "5.5.0", features = ["unit-testing"] } \ No newline at end of file diff --git a/test-contract-defi/src/lib.rs b/tests/contracts/defi/src/lib.rs similarity index 60% rename from test-contract-defi/src/lib.rs rename to tests/contracts/defi/src/lib.rs index d4a7569..83671a1 100644 --- a/test-contract-defi/src/lib.rs +++ b/tests/contracts/defi/src/lib.rs @@ -1,35 +1,36 @@ /*! Some hypothetical DeFi contract that will do smart things with the transferred tokens */ -use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_contract_standards::fungible_token::{receiver::FungibleTokenReceiver, Balance}; use near_sdk::json_types::U128; -use near_sdk::{ - env, ext_contract, log, near_bindgen, AccountId, Balance, PanicOnDefault, PromiseOrValue, -}; +use near_sdk::{env, log, near, require, AccountId, Gas, PanicOnDefault, PromiseOrValue}; -#[near_bindgen] -#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +const BASE_GAS: u64 = 5_000_000_000_000; +const PROMISE_CALL: u64 = 5_000_000_000_000; +const GAS_FOR_FT_ON_TRANSFER: Gas = Gas::from_gas(BASE_GAS + PROMISE_CALL); + +#[derive(PanicOnDefault)] +#[near(contract_state)] pub struct DeFi { fungible_token_account_id: AccountId, } -// Defining cross-contract interface. This allows to create a new promise. -#[ext_contract(ext_self)] -pub trait ValueReturnTrait { +// Have to repeat the same trait for our own implementation. +#[allow(dead_code)] +trait ValueReturnTrait { fn value_please(&self, amount_to_return: String) -> PromiseOrValue; } -#[near_bindgen] +#[near] impl DeFi { #[init] pub fn new(fungible_token_account_id: AccountId) -> Self { - assert!(!env::state_exists(), "Already initialized"); + require!(!env::state_exists(), "Already initialized"); Self { fungible_token_account_id: fungible_token_account_id.into() } } } -#[near_bindgen] +#[near] impl FungibleTokenReceiver for DeFi { /// If given `msg: "take-my-money", immediately returns U128::From(0) /// Otherwise, makes a cross-contract call to own `value_please` function, passing `msg` @@ -41,24 +42,26 @@ impl FungibleTokenReceiver for DeFi { msg: String, ) -> PromiseOrValue { // Verifying that we were called by fungible token contract that we expect. - assert_eq!( - &env::predecessor_account_id(), - &self.fungible_token_account_id, + require!( + env::predecessor_account_id() == self.fungible_token_account_id, "Only supports the one fungible token contract" ); - log!("in {} tokens from @{} ft_on_transfer, msg = {}", amount.0, sender_id.as_ref(), msg); + log!("in {} tokens from @{} ft_on_transfer, msg = {}", amount.0, sender_id, msg); match msg.as_str() { "take-my-money" => PromiseOrValue::Value(U128::from(0)), _ => { - // Call ok_go with no attached deposit and all unspent GAS (weight of 1) - Self::ext(env::current_account_id()) - .value_please(msg).into() + let prepaid_gas = env::prepaid_gas(); + let account_id = env::current_account_id(); + Self::ext(account_id) + .with_static_gas(prepaid_gas.saturating_sub(GAS_FOR_FT_ON_TRANSFER)) + .value_please(msg) + .into() } } } } -#[near_bindgen] +#[near] impl ValueReturnTrait for DeFi { fn value_please(&self, amount_to_return: String) -> PromiseOrValue { log!("in value_please, amount_to_return = {}", amount_to_return); diff --git a/tests/init.rs b/tests/init.rs new file mode 100644 index 0000000..2e5979b --- /dev/null +++ b/tests/init.rs @@ -0,0 +1,87 @@ +use near_sdk::{json_types::U128, AccountId, NearToken}; +use near_workspaces::{Account, Contract, DevNetwork, Worker}; + +const INITIAL_BALANCE: NearToken = NearToken::from_near(30); +pub const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1); + +pub async fn init_accounts(root: &Account) -> anyhow::Result<(Account, Account, Account, Account)> { + // create accounts + let alice = root + .create_subaccount("alice") + .initial_balance(INITIAL_BALANCE) + .transact() + .await? + .into_result()?; + let bob = root + .create_subaccount("bob") + .initial_balance(INITIAL_BALANCE) + .transact() + .await? + .into_result()?; + let charlie = root + .create_subaccount("charlie") + .initial_balance(INITIAL_BALANCE) + .transact() + .await? + .into_result()?; + let dave = root + .create_subaccount("dave") + .initial_balance(INITIAL_BALANCE) + .transact() + .await? + .into_result()?; + + return Ok((alice, bob, charlie, dave)); +} + +pub async fn init_contracts( + worker: &Worker, + initial_balance: U128, + account: &Account, +) -> anyhow::Result<(Contract, Contract)> { + let ft_wasm = near_workspaces::compile_project(".").await?; + let ft_contract = worker.dev_deploy(&ft_wasm).await?; + + let res = ft_contract + .call("new_default_meta") + .args_json((ft_contract.id(), initial_balance)) + .max_gas() + .transact() + .await?; + assert!(res.is_success()); + + let defi_wasm = near_workspaces::compile_project("./tests/contracts/defi").await?; + let defi_contract = worker.dev_deploy(&defi_wasm).await?; + + let res = defi_contract + .call("new") + .args_json((ft_contract.id(),)) + .max_gas() + .transact() + .await?; + assert!(res.is_success()); + + let res = ft_contract + .call("storage_deposit") + .args_json((account.id(), Option::::None)) + .deposit(near_sdk::env::storage_byte_cost().saturating_mul(125)) + .max_gas() + .transact() + .await?; + assert!(res.is_success()); + + return Ok((ft_contract, defi_contract)); +} + +pub async fn register_user(contract: &Contract, account_id: &AccountId) -> anyhow::Result<()> { + let res = contract + .call("storage_deposit") + .args_json((account_id, Option::::None)) + .max_gas() + .deposit(near_sdk::env::storage_byte_cost().saturating_mul(125)) + .transact() + .await?; + assert!(res.is_success()); + + Ok(()) +} diff --git a/tests/main.rs b/tests/main.rs new file mode 100644 index 0000000..ee78f7f --- /dev/null +++ b/tests/main.rs @@ -0,0 +1,2 @@ +mod init; +mod tests; diff --git a/tests/tests/mod.rs b/tests/tests/mod.rs new file mode 100644 index 0000000..61486dd --- /dev/null +++ b/tests/tests/mod.rs @@ -0,0 +1,3 @@ +mod storage; +mod supply; +mod transfer; diff --git a/tests/tests/storage.rs b/tests/tests/storage.rs new file mode 100644 index 0000000..31d236a --- /dev/null +++ b/tests/tests/storage.rs @@ -0,0 +1,292 @@ +use near_sdk::{json_types::U128, NearToken}; + +use crate::init::{init_accounts, init_contracts, ONE_YOCTO}; + +#[tokio::test] +async fn storage_deposit_not_enough_deposit() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let new_account = ft_contract + .as_account() + .create_subaccount("new-account") + .initial_balance(NearToken::from_near(10)) + .transact() + .await? + .into_result()?; + + let new_account_balance_before_deposit = new_account.view_account().await?.balance; + let contract_balance_before_deposit = ft_contract.view_account().await?.balance; + + let minimal_deposit = near_sdk::env::storage_byte_cost().saturating_mul(125); + let res = new_account + .call(ft_contract.id(), "storage_deposit") + .args(b"{}".to_vec()) + .max_gas() + .deposit(minimal_deposit.saturating_sub(NearToken::from_yoctonear(1))) + .transact() + .await?; + assert!(res.is_failure()); + + let new_account_balance_diff = new_account_balance_before_deposit + .saturating_sub(new_account.view_account().await?.balance); + // new_account is charged the transaction fee, so it should loose some NEAR + assert!(new_account_balance_diff > NearToken::from_near(0)); + assert!(new_account_balance_diff < NearToken::from_millinear(1)); + + let contract_balance_diff = ft_contract + .view_account() + .await? + .balance + .saturating_sub(contract_balance_before_deposit); + // contract receives a gas rewards for the function call, so it should gain some NEAR + assert!(contract_balance_diff > NearToken::from_near(0)); + assert!(contract_balance_diff < NearToken::from_yoctonear(30_000_000_000_000_000_000)); + + Ok(()) +} + +#[tokio::test] +async fn storage_deposit_minimal_deposit() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let new_account = ft_contract + .as_account() + .create_subaccount("new-account") + .initial_balance(NearToken::from_near(10)) + .transact() + .await? + .into_result()?; + + let new_account_balance_before_deposit = new_account.view_account().await?.balance; + let contract_balance_before_deposit = ft_contract.view_account().await?.balance; + + let minimal_deposit = near_sdk::env::storage_byte_cost().saturating_mul(125); + new_account + .call(ft_contract.id(), "storage_deposit") + .args(b"{}".to_vec()) + .max_gas() + .deposit(minimal_deposit) + .transact() + .await? + .into_result()?; + + let new_account_balance_diff = new_account_balance_before_deposit + .saturating_sub(new_account.view_account().await?.balance); + // new_account is charged the transaction fee, so it should loose a bit more than minimal_deposit + assert!(new_account_balance_diff > minimal_deposit); + assert!( + new_account_balance_diff < minimal_deposit.saturating_add(NearToken::from_millinear(1)) + ); + + let contract_balance_diff = ft_contract + .view_account() + .await? + .balance + .saturating_sub(contract_balance_before_deposit); + // contract receives a gas rewards for the function call, so the difference should be slightly more than minimal_deposit + assert!(contract_balance_diff > minimal_deposit); + // adjust the upper limit of the assertion to be more flexible for small variations in the gas reward received + assert!( + contract_balance_diff + < minimal_deposit.saturating_add(NearToken::from_yoctonear(50_000_000_000_000_000_000)) + ); + + Ok(()) +} + +#[tokio::test] +async fn storage_deposit_refunds_excessive_deposit() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let minimal_deposit = near_sdk::env::storage_byte_cost().saturating_mul(125); + + // Check the storage balance bounds to make sure we have the right minimal deposit + // + #[derive(near_sdk::serde::Serialize, near_sdk::serde::Deserialize)] + #[serde(crate = "near_sdk::serde")] + struct StorageBalanceBounds { + min: U128, + max: U128, + } + let storage_balance_bounds: StorageBalanceBounds = ft_contract + .call("storage_balance_bounds") + .view() + .await? + .json()?; + assert_eq!( + storage_balance_bounds.min, + minimal_deposit.as_yoctonear().into() + ); + assert_eq!( + storage_balance_bounds.max, + minimal_deposit.as_yoctonear().into() + ); + + // Check that a non-registered account does not have storage balance + // + #[derive(near_sdk::serde::Serialize, near_sdk::serde::Deserialize)] + #[serde(crate = "near_sdk::serde")] + struct StorageBalanceOf { + total: U128, + available: U128, + } + let storage_balance_bounds: Option = ft_contract + .call("storage_balance_of") + .args_json(near_sdk::serde_json::json!({"account_id": "non-registered-account"})) + .view() + .await? + .json()?; + assert!(storage_balance_bounds.is_none()); + + // Create a new account and deposit some NEAR to cover the storage + // + let new_account = ft_contract + .as_account() + .create_subaccount("new-account") + .initial_balance(NearToken::from_near(10)) + .transact() + .await? + .into_result()?; + + let new_account_balance_before_deposit = new_account.view_account().await?.balance; + let contract_balance_before_deposit = ft_contract.view_account().await?.balance; + + new_account + .call(ft_contract.id(), "storage_deposit") + .args(b"{}".to_vec()) + .max_gas() + .deposit(NearToken::from_near(5)) + .transact() + .await? + .into_result()?; + + // The expected storage balance should be the minimal deposit, + // the balance of the account should be reduced by the deposit, + // and the contract should gain the deposit. + // + let storage_balance_bounds: StorageBalanceOf = ft_contract + .call("storage_balance_of") + .args_json(near_sdk::serde_json::json!({"account_id": new_account.id()})) + .view() + .await? + .json()?; + assert_eq!( + storage_balance_bounds.total, + minimal_deposit.as_yoctonear().into() + ); + assert_eq!(storage_balance_bounds.available, 0.into()); + + let new_account_balance_diff = new_account_balance_before_deposit + .saturating_sub(new_account.view_account().await?.balance); + // new_account is charged the transaction fee, so it should loose a bit more than minimal_deposit + assert!(new_account_balance_diff > minimal_deposit); + assert!( + new_account_balance_diff < minimal_deposit.saturating_add(NearToken::from_millinear(1)) + ); + + let contract_balance_diff = ft_contract + .view_account() + .await? + .balance + .saturating_sub(contract_balance_before_deposit); + // contract receives a gas rewards for the function call, so the difference should be slightly more than minimal_deposit + assert!(contract_balance_diff > minimal_deposit); + assert!( + contract_balance_diff + < minimal_deposit.saturating_add(NearToken::from_yoctonear(50_000_000_000_000_000_000)) + ); + + Ok(()) +} + +#[tokio::test] +async fn close_account_empty_balance() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let res = alice + .call(ft_contract.id(), "storage_unregister") + .args_json((Option::::None,)) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await?; + assert!(res.json::()?); + + Ok(()) +} + +#[tokio::test] +async fn close_account_non_empty_balance() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let res = ft_contract + .call("storage_unregister") + .args_json((Option::::None,)) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await; + assert!(format!("{:?}", res) + .contains("Can't unregister the account with the positive balance without force")); + + let res = ft_contract + .call("storage_unregister") + .args_json((Some(false),)) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await; + assert!(format!("{:?}", res) + .contains("Can't unregister the account with the positive balance without force")); + + Ok(()) +} + +#[tokio::test] +async fn close_account_force_non_empty_balance() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let res = ft_contract + .call("storage_unregister") + .args_json((Some(true),)) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await?; + assert!(res.is_success()); + + let res = ft_contract.call("ft_total_supply").view().await?; + assert_eq!(res.json::()?.0, 0); + + Ok(()) +} diff --git a/tests/tests/supply.rs b/tests/tests/supply.rs new file mode 100644 index 0000000..25847eb --- /dev/null +++ b/tests/tests/supply.rs @@ -0,0 +1,18 @@ +use near_sdk::{json_types::U128, NearToken}; + +use crate::init::{init_accounts, init_contracts}; + +#[tokio::test] +async fn test_total_supply() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let res = ft_contract.call("ft_total_supply").view().await?; + assert_eq!(res.json::()?, initial_balance); + + Ok(()) +} diff --git a/tests/tests/transfer.rs b/tests/tests/transfer.rs new file mode 100644 index 0000000..2d5851f --- /dev/null +++ b/tests/tests/transfer.rs @@ -0,0 +1,301 @@ +use near_sdk::{json_types::U128, NearToken}; +use near_workspaces::{operations::Function, result::ValueOrReceiptId}; + +use crate::init::{init_accounts, init_contracts, register_user, ONE_YOCTO}; + +#[tokio::test] +async fn simple_transfer() -> anyhow::Result<()> { + // Create balance variables + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; + + let res = ft_contract + .call("ft_transfer") + .args_json((alice.id(), transfer_amount, Option::::None)) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await?; + assert!(res.is_success()); + + let ft_contract_balance = ft_contract + .call("ft_balance_of") + .args_json((ft_contract.id(),)) + .view() + .await? + .json::()?; + let alice_balance = ft_contract + .call("ft_balance_of") + .args_json((alice.id(),)) + .view() + .await? + .json::()?; + assert_eq!(initial_balance.0 - transfer_amount.0, ft_contract_balance.0); + assert_eq!(transfer_amount.0, alice_balance.0); + + Ok(()) +} + +#[tokio::test] +async fn transfer_call_with_burned_amount() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; + + // defi contract must be registered as a FT account + register_user(&ft_contract, defi_contract.id()).await?; + + // root invests in defi by calling `ft_transfer_call` + let res = ft_contract + .batch() + .call( + Function::new("ft_transfer_call") + .args_json(( + defi_contract.id(), + transfer_amount, + Option::::None, + "10", + )) + .deposit(ONE_YOCTO) + .gas(near_sdk::Gas::from_tgas(150)), + ) + .call( + Function::new("storage_unregister") + .args_json((Some(true),)) + .deposit(ONE_YOCTO) + .gas(near_sdk::Gas::from_tgas(150)), + ) + .transact() + .await?; + assert!(res.is_success()); + + let logs = res.logs(); + let expected = format!("Account @{} burned {}", ft_contract.id(), 10); + assert!(logs.len() >= 2); + assert!(logs.contains(&"The account of the sender was deleted")); + assert!(logs.contains(&(expected.as_str()))); + + match res.receipt_outcomes()[5].clone().into_result()? { + ValueOrReceiptId::Value(val) => { + let used_amount = val.json::()?; + assert_eq!(used_amount, transfer_amount); + } + _ => panic!("Unexpected receipt id"), + } + assert!(res.json::()?); + + let res = ft_contract.call("ft_total_supply").view().await?; + assert_eq!(res.json::()?.0, transfer_amount.0 - 10); + let defi_balance = ft_contract + .call("ft_balance_of") + .args_json((defi_contract.id(),)) + .view() + .await? + .json::()?; + assert_eq!(defi_balance.0, transfer_amount.0 - 10); + + Ok(()) +} + +#[tokio::test] +async fn transfer_call_with_immediate_return_and_no_refund() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; + + // defi contract must be registered as a FT account + register_user(&ft_contract, defi_contract.id()).await?; + + // root invests in defi by calling `ft_transfer_call` + let res = ft_contract + .call("ft_transfer_call") + .args_json(( + defi_contract.id(), + transfer_amount, + Option::::None, + "take-my-money", + )) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await?; + assert!(res.is_success()); + + let root_balance = ft_contract + .call("ft_balance_of") + .args_json((ft_contract.id(),)) + .view() + .await? + .json::()?; + let defi_balance = ft_contract + .call("ft_balance_of") + .args_json((defi_contract.id(),)) + .view() + .await? + .json::()?; + assert_eq!(initial_balance.0 - transfer_amount.0, root_balance.0); + assert_eq!(transfer_amount.0, defi_balance.0); + + Ok(()) +} + +#[tokio::test] +async fn transfer_call_when_called_contract_not_registered_with_ft() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; + + // call fails because DEFI contract is not registered as FT user + let res = ft_contract + .call("ft_transfer_call") + .args_json(( + defi_contract.id(), + transfer_amount, + Option::::None, + "take-my-money", + )) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await?; + assert!(res.is_failure()); + + // balances remain unchanged + let root_balance = ft_contract + .call("ft_balance_of") + .args_json((ft_contract.id(),)) + .view() + .await? + .json::()?; + let defi_balance = ft_contract + .call("ft_balance_of") + .args_json((defi_contract.id(),)) + .view() + .await? + .json::()?; + assert_eq!(initial_balance.0, root_balance.0); + assert_eq!(0, defi_balance.0); + + Ok(()) +} + +#[tokio::test] +async fn transfer_call_with_promise_and_refund() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + let refund_amount = U128::from(NearToken::from_near(50).as_yoctonear()); + let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); + + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; + + // defi contract must be registered as a FT account + register_user(&ft_contract, defi_contract.id()).await?; + + let res = ft_contract + .call("ft_transfer_call") + .args_json(( + defi_contract.id(), + transfer_amount, + Option::::None, + refund_amount.0.to_string(), + )) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await?; + assert!(res.is_success()); + + let root_balance = ft_contract + .call("ft_balance_of") + .args_json((ft_contract.id(),)) + .view() + .await? + .json::()?; + let defi_balance = ft_contract + .call("ft_balance_of") + .args_json((defi_contract.id(),)) + .view() + .await? + .json::()?; + assert_eq!( + initial_balance.0 - transfer_amount.0 + refund_amount.0, + root_balance.0 + ); + assert_eq!(transfer_amount.0 - refund_amount.0, defi_balance.0); + + Ok(()) +} + +#[tokio::test] +async fn transfer_call_promise_panics_for_a_full_refund() -> anyhow::Result<()> { + let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); + let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); + let worker = near_workspaces::sandbox().await?; + let root = worker.root_account()?; + let (alice, _, _, _) = init_accounts(&root).await?; + let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; + + // defi contract must be registered as a FT account + register_user(&ft_contract, defi_contract.id()).await?; + + // root invests in defi by calling `ft_transfer_call` + let res = ft_contract + .call("ft_transfer_call") + .args_json(( + defi_contract.id(), + transfer_amount, + Option::::None, + "no parsey as integer big panic oh no".to_string(), + )) + .max_gas() + .deposit(ONE_YOCTO) + .transact() + .await?; + assert!(res.is_success()); + + let promise_failures = res.receipt_failures(); + assert_eq!(promise_failures.len(), 1); + let failure = promise_failures[0].clone().into_result(); + if let Err(err) = failure { + assert!(format!("{:?}", err).contains("ParseIntError")); + } else { + unreachable!(); + } + + // balances remain unchanged + let root_balance = ft_contract + .call("ft_balance_of") + .args_json((ft_contract.id(),)) + .view() + .await? + .json::()?; + let defi_balance = ft_contract + .call("ft_balance_of") + .args_json((defi_contract.id(),)) + .view() + .await? + .json::()?; + assert_eq!(initial_balance, root_balance); + assert_eq!(0, defi_balance.0); + + Ok(()) +}