diff --git a/Cargo.toml b/Cargo.toml index 3c7d3db..d991e29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "contract" +name = "token-factory" description = "Factory Contract Example" -version = "0.1.0" +version = "1.0.0" edition = "2021" # TODO: Fill out the repository field to help NEAR ecosystem tools to discover your project. # NEP-0330 is automatically implemented for all contracts built with https://github.com/near/cargo-near. @@ -13,10 +13,11 @@ crate-type = ["cdylib", "rlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -near-sdk = { version = "5.1.0", features = ["unstable"] } +near-sdk = { version = "5.3.0", features = ["unstable"] } +near-contract-standards = "5.3.0" [dev-dependencies] -near-sdk = { version = "5.1.0", features = ["unit-testing"] } +near-sdk = { version = "5.3.0", features = ["unit-testing"] } near-workspaces = { version = "0.10.0", features = ["unstable"] } tokio = { version = "1.12.0", features = ["full"] } serde_json = "1" diff --git a/src/deploy.rs b/src/deploy.rs index d921ce0..39484e2 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -1,87 +1,102 @@ -use near_sdk::serde::Serialize; -use near_sdk::{env, log, near, AccountId, NearToken, Promise, PromiseError, PublicKey}; +use near_contract_standards::fungible_token::metadata::FungibleTokenMetadata; +use near_sdk::{borsh, env, json_types::U128, log, near, require, serde_json::json, AccountId, NearToken, Promise, PromiseError}; -use crate::{Contract, ContractExt, NEAR_PER_STORAGE, NO_DEPOSIT, TGAS}; +use crate::{Contract, ContractExt, FT_CONTRACT, NO_DEPOSIT, TGAS}; -#[derive(Serialize)] -#[serde(crate = "near_sdk::serde")] -struct DonationInitArgs { - beneficiary: AccountId, +type TokenId = String; + +const EXTRA_BYTES: usize = 10000; + +#[near(serializers = [json, borsh])] +pub struct TokenArgs { + owner_id: AccountId, + total_supply: U128, + metadata: FungibleTokenMetadata, +} + +pub fn is_valid_token_id(token_id: &TokenId) -> bool { + for c in token_id.as_bytes() { + match c { + b'0'..=b'9' | b'a'..=b'z' => (), + _ => return false, + } + } + true } #[near] impl Contract { + pub fn get_required(&self, args: &TokenArgs) -> NearToken { + env::storage_byte_cost().saturating_mul( + (FT_CONTRACT.len() + EXTRA_BYTES + borsh::to_vec(args).unwrap().len()) + .try_into() + .unwrap(), + ) + } + #[payable] - pub fn create_factory_subaccount_and_deploy( - &mut self, - name: String, - beneficiary: AccountId, - public_key: Option, - ) -> Promise { + pub fn create_token(&mut self, args: TokenArgs) -> Promise { + args.metadata.assert_valid(); + let token_id = args.metadata.symbol.to_ascii_lowercase(); + + require!(is_valid_token_id(&token_id), "Invalid Symbol"); + // Assert the sub-account is valid - let current_account = env::current_account_id().to_string(); - let subaccount: AccountId = format!("{name}.{current_account}").parse().unwrap(); - assert!( - env::is_valid_account_id(subaccount.as_bytes()), - "Invalid subaccount" + let token_account_id = format!("{}.{}", token_id, env::current_account_id()); + require!( + env::is_valid_account_id(token_account_id.as_bytes()), + "Token Account ID is invalid" ); // Assert enough tokens are attached to create the account and deploy the contract let attached = env::attached_deposit(); + let required = self.get_required(&args); - let code = self.code.clone().unwrap(); - let contract_bytes = code.len() as u128; - let minimum_needed = NEAR_PER_STORAGE.saturating_mul(contract_bytes); - assert!( - attached >= minimum_needed, - "Attach at least {minimum_needed} yⓃ" + require!( + attached >= required, + format!("Attach at least {required} yⓃ") ); - let init_args = near_sdk::serde_json::to_vec(&DonationInitArgs { beneficiary }).unwrap(); + let init_args = near_sdk::serde_json::to_vec(&args).unwrap(); + + let user = env::predecessor_account_id(); + let callback_args = json!({ "user": user, "deposit": attached }) + .to_string() + .into_bytes() + .to_vec(); - let mut promise = Promise::new(subaccount.clone()) + Promise::new(token_account_id.parse().unwrap()) .create_account() .transfer(attached) - .deploy_contract(code) + .deploy_contract(FT_CONTRACT.to_vec()) .function_call( - "init".to_owned(), + "new".to_owned(), init_args, NO_DEPOSIT, - TGAS.saturating_mul(5), - ); - - // Add full access key is the user passes one - if let Some(pk) = public_key { - promise = promise.add_full_access_key(pk); - } - - // Add callback - promise.then( - Self::ext(env::current_account_id()).create_factory_subaccount_and_deploy_callback( - subaccount, - env::predecessor_account_id(), - attached, - ), - ) + TGAS.saturating_mul(50), + ) + .then(Promise::new(env::current_account_id()).function_call( + "create_callback".to_string(), + callback_args, + NearToken::from_near(0), + TGAS.saturating_mul(30), + )) } #[private] - pub fn create_factory_subaccount_and_deploy_callback( - &mut self, - account: AccountId, + pub fn create_callback( + &self, user: AccountId, - attached: NearToken, - #[callback_result] create_deploy_result: Result<(), PromiseError>, + deposit: NearToken, + #[callback_result] call_result: Result<(), PromiseError>, ) -> bool { - if let Ok(_result) = create_deploy_result { - log!(format!("Correctly created and deployed to {account}")); - return true; - }; - - log!(format!( - "Error creating {account}, returning {attached}yⓃ to {user}" - )); - Promise::new(user).transfer(attached); - false + match call_result { + Ok(_) => true, + Err(e) => { + log!("Error creating token: {:?}", e); + Promise::new(user).transfer(deposit); + false + } + } } } diff --git a/src/donation-contract/donation.wasm b/src/donation-contract/donation.wasm deleted file mode 100755 index 561ae4e..0000000 Binary files a/src/donation-contract/donation.wasm and /dev/null differ diff --git a/src/ft-contract/ft.wasm b/src/ft-contract/ft.wasm new file mode 100755 index 0000000..c4acd42 Binary files /dev/null and b/src/ft-contract/ft.wasm differ diff --git a/src/lib.rs b/src/lib.rs index a3cc42e..832e257 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,31 +1,19 @@ // Find all our documentation at https://docs.near.org -use near_sdk::store::LazyOption; use near_sdk::{near, Gas, NearToken}; mod deploy; -mod manager; -const NEAR_PER_STORAGE: NearToken = NearToken::from_yoctonear(10u128.pow(18)); // 10e18yⓃ -const DEFAULT_CONTRACT: &[u8] = include_bytes!("./donation-contract/donation.wasm"); +const FT_CONTRACT: &[u8] = include_bytes!("./ft-contract/ft.wasm"); const TGAS: Gas = Gas::from_tgas(1); // 10e12yⓃ const NO_DEPOSIT: NearToken = NearToken::from_near(0); // 0yⓃ // Define the contract structure #[near(contract_state)] -pub struct Contract { - // Since a contract is something big to store, we use LazyOptions - // this way it is not deserialized on each method call - code: LazyOption>, - // Please note that it is much more efficient to **not** store this - // code in the state, and directly use `DEFAULT_CONTRACT` - // However, this does not enable to update the stored code. -} +pub struct Contract {} // Define the default, which automatically initializes the contract impl Default for Contract { fn default() -> Self { - Self { - code: LazyOption::new("code".as_bytes(), Some(DEFAULT_CONTRACT.to_vec())), - } + Self {} } } diff --git a/src/manager.rs b/src/manager.rs deleted file mode 100644 index 63dc858..0000000 --- a/src/manager.rs +++ /dev/null @@ -1,19 +0,0 @@ -use near_sdk::{env, near}; - -use crate::{Contract, ContractExt}; - -#[near] -impl Contract { - #[private] - pub fn update_stored_contract(&mut self) { - // This method receives the code to be stored in the contract directly - // from the contract's input. In this way, it avoids the overhead of - // deserializing parameters, which would consume a huge amount of GAS - self.code.set(env::input()); - } - - pub fn get_code(&self) -> &Vec { - // If a contract wants to update themselves, they can ask for the code needed - self.code.get().as_ref().unwrap() - } -} diff --git a/tests/sandbox.rs b/tests/sandbox.rs index a6f89dd..35eb3a6 100644 --- a/tests/sandbox.rs +++ b/tests/sandbox.rs @@ -1,52 +1,236 @@ -use near_workspaces::types::{AccountId, KeyType, NearToken, SecretKey}; +use near_contract_standards::fungible_token::metadata::FungibleTokenMetadata; +use near_sdk::{json_types::U128, near}; +use near_workspaces::{types::NearToken, Account, AccountId, Contract}; use serde_json::json; +#[near(serializers = [json, borsh])] +struct TokenArgs { + owner_id: AccountId, + total_supply: U128, + metadata: FungibleTokenMetadata, +} + #[tokio::test] async fn main() -> Result<(), Box> { let sandbox = near_workspaces::sandbox().await?; let contract_wasm = near_workspaces::compile_project("./").await?; let contract = sandbox.dev_deploy(&contract_wasm).await?; + let root = sandbox.root_account().unwrap(); + + let token_owner_account = root + .create_subaccount("the-token-owner-account-1234567890123456789") + .initial_balance(NearToken::from_near(5)) + .transact() + .await? + .into_result()?; + + let alice_account = root + .create_subaccount("alice") + .initial_balance(NearToken::from_near(5)) + .transact() + .await? + .into_result()?; - let alice = sandbox - .create_tla( - "alice.test.near".parse().unwrap(), - SecretKey::from_random(KeyType::ED25519), - ) + let bob_account = root + .create_subaccount("bob") + .initial_balance(NearToken::from_near(5)) + .transact() .await? - .unwrap(); + .into_result()?; + + create_token( + &contract, + &token_owner_account, + &alice_account, + &bob_account, + ) + .await?; + + Ok(()) +} + +async fn create_token( + factory: &Contract, + token_owner_account: &Account, + alice_account: &Account, + bob_account: &Account, +) -> Result<(), Box> { + // Initial setup + let symbol = "SOMETHING"; + let total_supply = U128(100); + let token_id = symbol.to_ascii_lowercase(); + let metadata = FungibleTokenMetadata { + spec: "ft-1.0.0".to_string(), + name: "The Something Token".to_string(), + symbol: symbol.to_string(), + decimals: 6, + icon: Some("data:image/svg+xml,%3Csvg width='111' height='90' viewBox='0 0 111 90' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M24.4825 0.862305H88.0496C89.5663 0.862305 90.9675 1.64827 91.7239 2.92338L110.244 34.1419C111.204 35.7609 110.919 37.8043 109.549 39.1171L58.5729 87.9703C56.9216 89.5528 54.2652 89.5528 52.6139 87.9703L1.70699 39.1831C0.305262 37.8398 0.0427812 35.7367 1.07354 34.1077L20.8696 2.82322C21.6406 1.60483 23.0087 0.862305 24.4825 0.862305ZM79.8419 14.8003V23.5597H61.7343V29.6329C74.4518 30.2819 83.9934 32.9475 84.0642 36.1425L84.0638 42.803C83.993 45.998 74.4518 48.6635 61.7343 49.3125V64.2168H49.7105V49.3125C36.9929 48.6635 27.4513 45.998 27.3805 42.803L27.381 36.1425C27.4517 32.9475 36.9929 30.2819 49.7105 29.6329V23.5597H31.6028V14.8003H79.8419ZM55.7224 44.7367C69.2943 44.7367 80.6382 42.4827 83.4143 39.4727C81.0601 36.9202 72.5448 34.9114 61.7343 34.3597V40.7183C59.7966 40.8172 57.7852 40.8693 55.7224 40.8693C53.6595 40.8693 51.6481 40.8172 49.7105 40.7183V34.3597C38.8999 34.9114 30.3846 36.9202 28.0304 39.4727C30.8066 42.4827 42.1504 44.7367 55.7224 44.7367Z' fill='%23009393'/%3E%3C/svg%3E".to_string()), + reference: None, + reference_hash: None, + }; - let bob = sandbox.dev_create_account().await?; + let token_args = TokenArgs { + owner_id: token_owner_account.id().clone(), + total_supply, + metadata, + }; - let res = contract - .call("create_factory_subaccount_and_deploy") - .args_json(json!({"name": "donation_for_alice", "beneficiary": alice.id()})) + // Getting required deposit based on provided arguments + let required_deposit: U128 = factory + .view("get_required") + .args_json(json!({"args": token_args})) + .await? + .json()?; + + // Creating token with less than required deposit (should fail) + let not_enough = alice_account + .call(factory.id(), "create_token") + .args_json(json!({"args": token_args})) .max_gas() - .deposit(NearToken::from_near(5)) + .deposit(NearToken::from_yoctonear(required_deposit.0 - 1)) .transact() .await?; + assert!(not_enough.is_failure()); - assert!(res.is_success()); + // Creating token with the required deposit + let alice_succeeds = alice_account + .call(factory.id(), "create_token") + .args_json(json!({"args": token_args})) + .max_gas() + .deposit(NearToken::from_yoctonear(required_deposit.0)) + .transact() + .await?; + assert!(alice_succeeds.json::()? == true); - let sub_accountid: AccountId = format!("donation_for_alice.{}", contract.id()) - .parse() - .unwrap(); + // Creating same token fails + let bob_balance = bob_account.view_account().await?.balance; - let res = bob - .view(&sub_accountid, "get_beneficiary") - .args_json({}) + let bob_fails = bob_account + .call(factory.id(), "create_token") + .args_json(json!({"args": token_args})) + .max_gas() + .deposit(NearToken::from_yoctonear(required_deposit.0)) + .transact() .await?; - assert_eq!(res.json::()?, alice.id().clone()); + let bob_balance_after = bob_account.view_account().await?.balance; + let rest = bob_balance.saturating_sub(bob_balance_after).as_millinear(); + println!("{:?}", rest); - let res = bob - .call(&sub_accountid, "donate") - .args_json({}) + // bob fails + assert!(bob_fails.json::()? == false); + + // but it gets back the money (i.e. looses less than 0.005 N) + assert!(rest < 5); + + // Checking created token account and metadata + let token_account_id: AccountId = format!("{}.{}", token_id, factory.id()).parse().unwrap(); + let token_metadata: FungibleTokenMetadata = token_owner_account + .view(&token_account_id, "ft_metadata") + .args_json(json!({})) + .await? + .json()?; + + assert_eq!(token_metadata.symbol, symbol); + + // Checking token supply + let token_total_supply: U128 = token_owner_account + .view(&token_account_id, "ft_total_supply") + .args_json(json!({})) + .await? + .json()?; + assert_eq!(token_total_supply.0, total_supply.0); + + // Checking total supply belongs to the owner account + let token_owner_balance: U128 = token_owner_account + .view(&token_account_id, "ft_balance_of") + .args_json(json!({"account_id": token_owner_account.id()})) + .await? + .json()?; + + assert_eq!(token_owner_balance.0, total_supply.0); + + // Checking transferring tokens from owner to other account + let _ = alice_account + .call(&token_account_id, "storage_deposit") + .args_json(json!({"account_id": alice_account.id()})) + .max_gas() + .deposit(NearToken::from_millinear(250)) + .transact() + .await?; + + let alice_balance_before: U128 = alice_account + .view(&token_account_id, "ft_balance_of") + .args_json(json!({"account_id": alice_account.id()})) + .await? + .json()?; + assert_eq!(alice_balance_before.0, 0); + + let _ = token_owner_account + .call(&token_account_id, "ft_transfer") + .args_json(json!({ + "receiver_id": alice_account.id(), + "amount": "2", + })) + .max_gas() + .deposit(NearToken::from_yoctonear(1)) + .transact() + .await?; + + let alice_balance_after: U128 = alice_account + .view(&token_account_id, "ft_balance_of") + .args_json(json!({"account_id": alice_account.id()})) + .await? + .json()?; + assert_eq!(alice_balance_after.0, 2); + + // Checking transferring token from alice to bob + let _ = bob_account + .call(&token_account_id, "storage_deposit") + .args_json(json!({"account_id": bob_account.id()})) .max_gas() - .deposit(NearToken::from_near(5)) + .deposit(NearToken::from_millinear(250)) .transact() .await?; + let bob_balance_before: U128 = bob_account + .view(&token_account_id, "ft_balance_of") + .args_json(json!({"account_id": bob_account.id()})) + .await? + .json()?; + assert_eq!(bob_balance_before.0, 0); + + let _ = alice_account + .call(&token_account_id, "ft_transfer") + .args_json(json!({ + "receiver_id": bob_account.id(), + "amount": "1", + })) + .max_gas() + .deposit(NearToken::from_yoctonear(1)) + .transact() + .await?; + let bob_balance_after: U128 = bob_account + .view(&token_account_id, "ft_balance_of") + .args_json(json!({"account_id": bob_account.id()})) + .await? + .json()?; + assert_eq!(bob_balance_after.0, 1); + + let alice_balance_after: U128 = alice_account + .view(&token_account_id, "ft_balance_of") + .args_json(json!({"account_id": alice_account.id()})) + .await? + .json()?; + assert_eq!(alice_balance_after.0, 1); + + // Checking total supply belongs to the owner account + let token_owner_balance: U128 = token_owner_account + .view(&token_account_id, "ft_balance_of") + .args_json(json!({"account_id": token_owner_account.id()})) + .await? + .json()?; - assert!(res.is_success()); + assert_eq!(token_owner_balance.0, total_supply.0 - 2); Ok(()) }