Skip to content

Commit 428ae8d

Browse files
authored
Merge pull request #17 from garikbesson/rust-version
Added rust version of contract, reorganized repo structure
2 parents 85e7b2c + ff26b7c commit 428ae8d

22 files changed

+303
-13092
lines changed

.gitignore

+14-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
node_modules
2-
build
3-
neardev
4-
yarn.lock
5-
.parcel-cache/
6-
dist
7-
**/videos
8-
**/screenshots
9-
.idea
1+
# Rust
2+
**/target
3+
**/Cargo.lock
4+
5+
# TypeScript
6+
**/package-lock.json
7+
**/node_modules/
8+
**/build/
9+
**/yarn.lock
10+
**/.tsimp
11+
12+
# Frontend
13+
**/dist/
14+
**/.parcel-cache

LICENSE

-21
This file was deleted.

contract-rs/Cargo.toml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "contract-rs"
3+
description = "cargo-near-new-project-description"
4+
version = "0.1.0"
5+
edition = "2021"
6+
# TODO: Fill out the repository field to help NEAR ecosystem tools to discover your project.
7+
# NEP-0330 is automatically implemented for all contracts built with https://github.com/near/cargo-near.
8+
# Link to the repository will be available via `contract_source_metadata` view-function.
9+
#repository = "https://github.com/xxx/xxx"
10+
11+
[lib]
12+
crate-type = ["cdylib", "rlib"]
13+
14+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15+
[dependencies]
16+
near-sdk = { version = "5.0.0", features = ["legacy"] }
17+
18+
[dev-dependencies]
19+
near-sdk = { version = "5.0.0", features = ["unit-testing"] }
20+
near-workspaces = { version = "0.10.0", features = ["unstable"] }
21+
tokio = { version = "1.12.0", features = ["full"] }
22+
serde_json = "1"
23+
24+
[profile.release]
25+
codegen-units = 1
26+
# Tell `rustc` to optimize for small code size.
27+
opt-level = "z"
28+
lto = true
29+
debug = false
30+
panic = "abort"
31+
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
32+
overflow-checks = true

contract-rs/README.md

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Coin Flip 🪙
2+
[![](https://img.shields.io/badge/⋈%20Examples-Basics-green)](https://docs.near.org/tutorials/welcome)
3+
[![](https://img.shields.io/badge/Gitpod-Ready-orange)](https://gitpod.io/#/https://github.com/near-examples/coin-flip-js)
4+
[![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fnear-examples%2Fcoin-flip-js%2Fbadge%3Fref%3Dmain&style=flat&label=Tests&ref=main)](https://actions-badge.atrox.dev/near-examples/coin-flip-js/goto?ref=main)
5+
6+
Coin Flip is a game were the player tries to guess the outcome of a coin flip. It is one of the simplest contracts implementing random numbers.
7+
8+
9+
# What This Example Shows
10+
11+
1. How to store and retrieve information in the NEAR network.
12+
2. How to integrate a smart contract in a web frontend.
13+
3. How to generate random numbers in a contract.
14+
15+
<br />
16+
17+
# Quickstart
18+
19+
1. Make sure you have installed [rust](https://rust.org/).
20+
2. Install the [`NEAR CLI`](https://github.com/near/near-cli#setup)
21+
22+
<br />
23+
24+
## 1. Build, Test and Deploy
25+
To build the contract you can execute the `./build.sh` script, which will in turn run:
26+
27+
```bash
28+
rustup target add wasm32-unknown-unknown
29+
cargo build --target wasm32-unknown-unknown --release
30+
```
31+
32+
Then, deploy your contract using following commands:
33+
34+
```bash
35+
export CONTRACT_ID=test.near
36+
near deploy "'$CONTRACT_ID'" ./target/wasm32-unknown-unknown/release/contract.wasm
37+
```
38+
39+
<br />
40+
41+
## 2. Retrieve the user points
42+
43+
`points_of` is a `view` method.
44+
45+
`View` methods can be called for **free** by anyone, even people **without a NEAR account**!
46+
47+
```bash
48+
# Use near-cli to get the greeting
49+
near view $CONTRACT_ID points_of '{"player": "'$CONTRACT_ID'"}'
50+
```
51+
52+
<br />
53+
54+
## 3. Flip a coin
55+
`flip_coin` changes the contract's state, for which it is a `call` method.
56+
57+
`Call` methods can only be invoked using a NEAR account, since the account needs to pay GAS for the transaction. In this case, we are asking the account we created in step 1 to sign the transaction.
58+
59+
```bash
60+
near call $CONTRACT_ID flip_coin '{"player_guess": "tails"}' --accountId $CONTRACT_ID
61+
```

contract-rs/rust-toolchain.toml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[toolchain]
2+
channel = "stable"
3+
components = ["rustfmt"]
4+
targets = ["wasm32-unknown-unknown"]

contract-rs/src/lib.rs

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Find all our documentation at https://docs.near.org
2+
use near_sdk::borsh::{BorshSerialize, BorshDeserialize};
3+
use near_sdk::env::{self, log_str};
4+
use near_sdk::{near_bindgen, AccountId, BorshStorageKey};
5+
use near_sdk::collections::UnorderedMap;
6+
7+
#[derive(BorshSerialize, BorshStorageKey)]
8+
#[borsh(crate = "near_sdk::borsh")]
9+
enum StorageKey {
10+
Points,
11+
}
12+
13+
pub(crate) fn simulate_coin_flip() -> String {
14+
// Here we get a first byte of a random seed
15+
let random_seed = *env::random_seed().get(0).unwrap() as i8;
16+
17+
// If a first byte is EVEN we choose heads, otherwise tails
18+
if let 0 = random_seed % 2 {
19+
return "heads".to_string()
20+
} else {
21+
return "tails".to_string()
22+
};
23+
}
24+
25+
// Define the contract structure
26+
#[near_bindgen]
27+
#[derive(BorshDeserialize, BorshSerialize)]
28+
#[borsh(crate = "near_sdk::borsh")]
29+
pub struct Contract {
30+
points: UnorderedMap<AccountId, u8>,
31+
}
32+
33+
// Define the default, which automatically initializes the contract
34+
impl Default for Contract {
35+
fn default() -> Self {
36+
Self {
37+
points: UnorderedMap::new(StorageKey::Points)
38+
}
39+
}
40+
}
41+
42+
// Implement the contract structure
43+
#[near_bindgen]
44+
impl Contract {
45+
/*
46+
Flip a coin. Pass in the side (heads or tails) and a random number will be chosen
47+
indicating whether the flip was heads or tails. If you got it right, you get a point.
48+
*/
49+
pub fn flip_coin(&mut self, player_guess: String) -> String {
50+
// Check who called the method
51+
let player: AccountId = env::predecessor_account_id();
52+
log_str(&format!("{player} chose {player_guess}"));
53+
54+
// Simulate a Coin Flip
55+
let outcome = simulate_coin_flip();
56+
57+
// Get the current player points
58+
let mut player_points = self.points.get(&player).unwrap_or(0);
59+
60+
// Check if their guess was right and modify the points accordingly
61+
if outcome.eq(&player_guess) {
62+
player_points = player_points + 1;
63+
} else {
64+
player_points = player_points.saturating_sub(1);
65+
};
66+
67+
log_str(&format!("player_points: {player_points}"));
68+
69+
// Store the new points
70+
self.points.insert(&player, &player_points);
71+
72+
return outcome;
73+
}
74+
75+
// View how many points a specific player has
76+
pub fn points_of(&self, player: AccountId) -> u8 {
77+
let points = self.points.get(&player).unwrap_or(0);
78+
log_str(&format!("Points for {player}: {points}"));
79+
80+
return points;
81+
}
82+
}

contract-rs/tests/tests.rs

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use near_workspaces::{types::NearToken, Account, Contract};
2+
use serde_json::json;
3+
4+
#[tokio::test]
5+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
6+
let worker = near_workspaces::sandbox().await?;
7+
let contract_wasm = near_workspaces::compile_project("./").await?;
8+
let contract = worker.dev_deploy(&contract_wasm).await?;
9+
10+
// create accounts
11+
let account = worker.dev_create_account().await?;
12+
let alice = account
13+
.create_subaccount("alice")
14+
.initial_balance(NearToken::from_near(30))
15+
.transact()
16+
.await?
17+
.into_result()?;
18+
19+
// begin tests
20+
test_user_has_no_points(&alice, &contract).await?;
21+
test_points_are_correctly_computed(&alice, &contract).await?;
22+
Ok(())
23+
}
24+
25+
async fn test_user_has_no_points(
26+
user: &Account,
27+
contract: &Contract,
28+
) -> Result<(), Box<dyn std::error::Error>> {
29+
let points: u8 = user
30+
.call(contract.id(), "points_of")
31+
.args_json(json!({ "player": user.id()}))
32+
.transact()
33+
.await?
34+
.json()?;
35+
36+
assert_eq!(points, 0);
37+
println!(" Passed ✅ test_user_has_no_points");
38+
Ok(())
39+
}
40+
41+
async fn test_points_are_correctly_computed(
42+
user: &Account,
43+
contract: &Contract,
44+
) -> Result<(), Box<dyn std::error::Error>> {
45+
let mut tails_counter = 0;
46+
let mut heads_counter = 0;
47+
let mut expected_points = 0;
48+
49+
let mut i = 0;
50+
while i < 10 {
51+
let outcome: String = user.call(contract.id(), "flip_coin")
52+
.args_json(json!({"player_guess": "tails"}))
53+
.transact()
54+
.await?
55+
.json()?;
56+
57+
if outcome.eq("tails") {
58+
tails_counter = tails_counter + 1;
59+
expected_points = expected_points + 1;
60+
} else {
61+
heads_counter = heads_counter + 1;
62+
if expected_points > 0 {
63+
expected_points = expected_points - 1;
64+
}
65+
}
66+
i = i + 1;
67+
}
68+
69+
assert!(heads_counter >= 2);
70+
assert!(tails_counter >= 2);
71+
72+
let points: u8 = user
73+
.call(contract.id(), "points_of")
74+
.args_json(json!({ "player": user.id()}))
75+
.transact()
76+
.await?
77+
.json()?;
78+
79+
assert_eq!(points, expected_points);
80+
println!(" Passed ✅ test_points_are_correctly_computed");
81+
Ok(())
82+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

integration-tests/package.json contract-ts/integration-tests/package.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
"test": "ava"
77
},
88
"devDependencies": {
9-
"@types/bn.js": "^5.1.0",
10-
"@types/node": "^18.6.2",
11-
"ava": "^4.2.0",
12-
"near-workspaces": "^3.2.1",
9+
"ava": "^4.3.3",
10+
"near-workspaces": "^3.2.2",
11+
"typescript": "^4.6.4",
1312
"ts-node": "^10.8.0",
14-
"typescript": "^4.7.2"
13+
"@types/bn.js": "^5.1.0"
1514
},
1615
"dependencies": {}
1716
}

integration-tests/src/main.ava.ts contract-ts/integration-tests/src/main.ava.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Worker, NearAccount } from 'near-workspaces';
22
import anyTest, { TestFn } from 'ava';
3+
import * as path from 'path';
34

45
const test = anyTest as TestFn<{
56
worker: Worker;
@@ -12,10 +13,10 @@ test.beforeEach(async (t) => {
1213

1314
// Deploy contract
1415
const root = worker.rootAccount;
15-
const contract = await root.createSubAccount('test-account');
1616

1717
// Get wasm file path from package.json test script in folder above
18-
await contract.deploy(process.argv[2]);
18+
const contractPath = path.join(__dirname, "../../build/contract.wasm");
19+
const contract = await root.devDeploy(contractPath);
1920

2021
// Save state for test runs, it is unique for each test
2122
t.context.worker = worker;

contract/package.json contract-ts/package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
"scripts": {
77
"build": "./build.sh",
88
"deploy": "./deploy.sh",
9-
"test": "echo no unit testing"
9+
"test": "npm run build && npm run test:integration",
10+
"test:integration": "cd integration-tests && npm test"
1011
},
1112
"dependencies": {
12-
"near-cli": "^3.4.0",
13+
"near-cli": "^4.0.10",
1314
"near-sdk-js": "1.0.0"
1415
},
1516
"devDependencies": {
16-
"typescript": "^4.8.4",
17-
"ts-morph": "^16.0.0"
17+
"ts-morph": "^21.0.1",
18+
"typescript": "^5.3.3"
1819
}
1920
}
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)