Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reference implementation for proposed Multi-token spec (NEP-245) #776

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,068 changes: 3,068 additions & 0 deletions examples/multi-token/Cargo.lock

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions examples/multi-token/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "multi-token-wrapper"
version = "0.1.0"
edition = "2021"

[dev-dependencies]
anyhow = "1.0"
near-primitives = "0.5.0"
near-sdk = "4.0.0-pre.6"
near-units = "0.2.0"
serde_json = "1.0"
tokio = { version = "1.14", features = ["full"] }
workspaces = "0.1.1"

# remember to include a line for each contract
multi-token = { path = "./mt" }
defi = { path = "./test-contract-defi" }
approval-receiver = { path = "./test-approval-receiver" }

[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true

[workspace]
# remember to include a member for each contract
members = [
"mt",
"test-contract-defi",
"test-approval-receiver",
]
9 changes: 9 additions & 0 deletions examples/multi-token/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
TARGET="${CARGO_TARGET_DIR:-target}"
set -e
cd "`dirname $0`"

cargo build --all --target wasm32-unknown-unknown --release
cp $TARGET/wasm32-unknown-unknown/release/defi.wasm ./res/
cp $TARGET/wasm32-unknown-unknown/release/multi_token.wasm ./res/
cp $TARGET/wasm32-unknown-unknown/release/approval_receiver.wasm ./res/
12 changes: 12 additions & 0 deletions examples/multi-token/mt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "multi-token"
version = "1.1.0"
authors = ["Near Inc <hello@nearprotocol.com>, @jriemann"]
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = { path = "../../../near-sdk" }
near-contract-standards = { path = "../../../near-contract-standards" }
313 changes: 313 additions & 0 deletions examples/multi-token/mt/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::LazyOption;
use near_sdk::json_types::U128;
use near_sdk::Promise;
use near_sdk::{
env, near_bindgen, require, AccountId, Balance, BorshStorageKey, PanicOnDefault, PromiseOrValue,
};
use near_contract_standards::multi_token::metadata::MT_METADATA_SPEC;
use near_contract_standards::multi_token::token::{Token, TokenId};
use near_contract_standards::multi_token::{
core::MultiToken,
metadata::{MtContractMetadata, TokenMetadata},
};

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct ExampleMTContract {
tokens: MultiToken,
metadata: LazyOption<MtContractMetadata>,
}

#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKey {
MultiToken,
Metadata,
TokenMetadata,
Enumeration,
Approval,
}

#[near_bindgen]
impl ExampleMTContract {
#[init]
pub fn new_default_meta(owner_id: AccountId) -> Self {
let metadata = MtContractMetadata {
spec: MT_METADATA_SPEC.to_string(),
name: "Test".to_string(),
symbol: "OMG".to_string(),
icon: None,
base_uri: None,
reference: None,
reference_hash: None,
};

Self::new(owner_id, metadata)
}

#[init]
pub fn new(owner_id: AccountId, metadata: MtContractMetadata) -> Self {
require!(!env::state_exists(), "Already initialized");
metadata.assert_valid();

Self {
tokens: MultiToken::new(
StorageKey::MultiToken,
owner_id,
Some(StorageKey::TokenMetadata),
Some(StorageKey::Enumeration),
Some(StorageKey::Approval),
),
metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)),
}
}

#[payable]
pub fn mt_mint(
&mut self,
token_owner_id: AccountId,
token_metadata: TokenMetadata,
amount: Balance,
) -> Token {
// Only the owner of the MT contract can perform this operation
assert_eq!(
env::predecessor_account_id(),
self.tokens.owner_id,
"Unauthorized: {} != {}",
env::predecessor_account_id(),
self.tokens.owner_id
);
self.tokens.internal_mint(token_owner_id, Some(amount), Some(token_metadata), None)
}

pub fn register(&mut self, token_id: TokenId, account_id: AccountId) {
self.tokens.internal_register_account(&token_id, &account_id)
}
}

near_contract_standards::impl_multi_token_core!(ExampleMTContract, tokens);
near_contract_standards::impl_multi_token_approval!(ExampleMTContract, tokens);
near_contract_standards::impl_multi_token_enumeration!(ExampleMTContract, tokens);

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::{accounts, VMContextBuilder};
use near_sdk::testing_env;

fn create_token_md(title: String, description: String) -> TokenMetadata {
TokenMetadata {
title: Some(title),
description: Some(description),
media: None,
media_hash: None,
issued_at: Some(String::from("123456")),
expires_at: None,
starts_at: None,
updated_at: None,
extra: None,
reference: None,
reference_hash: None,
}
}

#[test]
fn test_transfer() {
let mut context = VMContextBuilder::new();
set_caller(&mut context, 0);
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
let (token, _) = init_tokens(&mut contract);
contract.register(token.token_id.clone(), accounts(1));

// Initial balances are what we expect.
assert_eq!(
contract.mt_balance_of(accounts(0), token.token_id.clone()),
U128(1000),
"Wrong balance"
);
assert_eq!(
contract.mt_balance_of(accounts(1), token.token_id.clone()),
U128(0),
"Wrong balance"
);

// Transfer some tokens
testing_env!(context.attached_deposit(1).build());
contract.mt_transfer(accounts(1), token.token_id.clone(), 4.into(), None, None);

// Transfer should have succeeded.
assert_eq!(
contract.mt_balance_of(accounts(0), token.token_id.clone()).0,
996,
"Wrong balance"
);
assert_eq!(
contract.mt_balance_of(accounts(1), token.token_id.clone()).0,
4,
"Wrong balance"
);

// Transfer some of the tokens back to original owner.
set_caller(&mut context, 1);
contract.mt_transfer(accounts(0), token.token_id.clone(), 3.into(), None, None);

assert_eq!(
contract.mt_balance_of(accounts(0), token.token_id.clone()).0,
999,
"Wrong balance"
);
assert_eq!(
contract.mt_balance_of(accounts(1), token.token_id.clone()).0,
1,
"Wrong balance"
);
}

#[test]
#[should_panic(expected = "Transferred amounts must be greater than 0")]
fn test_transfer_amount_must_be_positive() {
let mut context = VMContextBuilder::new();
set_caller(&mut context, 0);
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
let (token, _) = init_tokens(&mut contract);
contract.register(token.token_id.clone(), accounts(1));
testing_env!(context.attached_deposit(1).build());

contract.mt_transfer(accounts(1), token.token_id.clone(), U128(0), None, None)
}

#[test]
#[should_panic(expected = "The account doesn't have enough balance")]
fn test_sender_account_must_have_sufficient_balance() {
let mut context = VMContextBuilder::new();
set_caller(&mut context, 0);
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
let (token, _) = init_tokens(&mut contract);
contract.register(token.token_id.clone(), accounts(1));
testing_env!(context.attached_deposit(1).build());

// account(0) has only 2000 of token.
contract.mt_transfer(accounts(1), token.token_id.clone(), U128(3000), None, None)
}

#[test]
#[should_panic(expected = "Requires attached deposit of exactly 1 yoctoNEAR")]
fn test_transfers_require_one_yocto() {
let mut context = VMContextBuilder::new();
set_caller(&mut context, 0);
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
let (token, _) = init_tokens(&mut contract);
contract.register(token.token_id.clone(), accounts(1));
contract.mt_transfer(accounts(1), token.token_id.clone(), U128(1000), None, None)
}

#[test]
#[should_panic(expected = "The account charlie is not registered")]
fn test_receiver_must_be_registered() {
let mut context = VMContextBuilder::new();
set_caller(&mut context, 0);
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
let (token, _) = init_tokens(&mut contract);
contract.register(token.token_id.clone(), accounts(1));
testing_env!(context.attached_deposit(1).build());

contract.mt_transfer(accounts(2), token.token_id.clone(), U128(100), None, None)
}

#[test]
#[should_panic(expected = "Sender and receiver must differ")]
fn test_cannot_transfer_to_self() {
let mut context = VMContextBuilder::new();
set_caller(&mut context, 0);
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
let (token, _) = init_tokens(&mut contract);
contract.register(token.token_id.clone(), accounts(1));
testing_env!(context.attached_deposit(1).build());

contract.mt_transfer(accounts(0), token.token_id.clone(), U128(100), None, None)
}

#[test]
fn test_batch_transfer() {
let mut context = VMContextBuilder::new();
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
set_caller(&mut context, 0);

let (quote_token, base_token) = init_tokens(&mut contract);

contract.register(quote_token.token_id.clone(), accounts(1));
contract.register(base_token.token_id.clone(), accounts(1));

testing_env!(context.attached_deposit(1).build());

// Perform the transfers
contract.mt_batch_transfer(
accounts(1),
vec![quote_token.token_id.clone(), base_token.token_id.clone()],
vec![U128(4), U128(600)],
None,
None,
);

assert_eq!(
contract.mt_balance_of(accounts(0), quote_token.token_id.clone()).0,
996,
"Wrong balance"
);
assert_eq!(
contract.mt_balance_of(accounts(1), quote_token.token_id.clone()).0,
4,
"Wrong balance"
);

assert_eq!(
contract.mt_balance_of(accounts(0), base_token.token_id.clone()).0,
1400,
"Wrong balance"
);
assert_eq!(
contract.mt_balance_of(accounts(1), base_token.token_id.clone()).0,
600,
"Wrong balance"
);
}

#[test]
#[should_panic(expected = "The account doesn't have enough balance")]
fn test_batch_transfer_all_balances_must_be_sufficient() {
let mut context = VMContextBuilder::new();
let mut contract = ExampleMTContract::new_default_meta(accounts(0));
set_caller(&mut context, 0);

let (quote_token, base_token) = init_tokens(&mut contract);

contract.register(quote_token.token_id.clone(), accounts(1));
contract.register(base_token.token_id.clone(), accounts(1));
testing_env!(context.attached_deposit(1).build());

contract.mt_batch_transfer(
accounts(1),
vec![quote_token.token_id.clone(), base_token.token_id.clone()],
vec![U128(4), U128(6000)],
None,
None,
);
}

fn init_tokens(contract: &mut ExampleMTContract) -> (Token, Token) {
let quote_token_md = create_token_md("PYC".into(), "Python token".into());
let base_token_md = create_token_md("ABC".into(), "Alphabet token".into());

let quote_token = contract.mt_mint(accounts(0), quote_token_md.clone(), 1000);
let base_token = contract.mt_mint(accounts(0), base_token_md.clone(), 2000);

(quote_token, base_token)
}

fn set_caller(context: &mut VMContextBuilder, account_id: usize) {
testing_env!(context
.signer_account_id(accounts(account_id))
.predecessor_account_id(accounts(account_id))
.build())
}
}
Binary file not shown.
Binary file added examples/multi-token/res/defi.wasm
Binary file not shown.
Binary file added examples/multi-token/res/multi_token.wasm
Binary file not shown.
12 changes: 12 additions & 0 deletions examples/multi-token/test-approval-receiver/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "approval-receiver"
version = "0.1.0"
authors = ["Near Inc <hello@nearprotocol.com>"]
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = { path = "../../../near-sdk" }
near-contract-standards = { path = "../../../near-contract-standards" }
Loading