Building a simple swap DApp On The NEAR Blockchain
You can find the codes and files in the Github repo
.
In this tutorial, we will go through the process of building a token swap exchange on the NEAR
blockchain.
We start by writing Token and Exchange smart contracts in Rust
and deploying the contracts on the NEAR
testnet chain using near-cli
.
Then we make a simple Web UI to interact with the Exchange, using the near-sdk-js
library.
Finally, we host our DApp on Skynet
.
Table of Contents
- Requirements
- Setting up near-cli
- Accounts & Wallet
- Setting up rust
- Getting started
- MLB1 contract
- Exchange contract
- Web UI
- Hosting On Skynet
- References
- About The Author
- Donations
Requirements
We will use the following technologies:
- NEAR CLI v2.2.0 or higher installed globally
- near-sdk v3 or higher installed globally
- Nodejs and NPM v14.17.6 LTS or higher installed
- Rust v1.56 or higher installed
- React v17.0.1 or higher installed
- near-api-js v0.43.1 or higher installed
- parcel v2 or higher installed
Setting up near-cli
NEAR CLI is a Node.js application that relies on near-api-js
to connect to and interact with the NEAR blockchain
. Create accounts, access keys, sign & send transactions with this command line interface tool.
sudo npm install -g near-cli
near login # Store testnet access keys locally
near login
will open the browser, testnet wallet page. You will login to the testnet and store the access keys locally.
Note: All the
near
commands work on the testnet chain (https://rpc.testnet.near.org), unless you specify theNEAR_ENV
variable or provide the--nodeUrl
flag.
Accounts & Wallet
In NEAR, accounts are string
, like mlibre.near
. Each account can have multiple keys-pair.
If you use near login
, near will save your key-pair from the wallet into your local computer. You can find them here:
cat .near-credentials/testnet/ACCOUNT_ID.near.json
You can also query an account's public keys by running:
near keys mlibre.testnet
Go ahead and create a testnet
wallet.
Setting up rust
Rust is a modern systems programming language, focused on safety, speed and concurrency. It is used to write high-performance, distributed systems. Rust also supports WebAssembly.
it is currently, the preferred programming language for writing smart contracts on NEAR.
install Rust, and add the wasm
to the rust toolchain.
# https://www.rust-lang.org/
rustup target add wasm32-unknown-unknown
Getting started
- First, we write a fungible token (FT) named
MLB1
in rust, and will deploy it on the chain. We use it as thenear-token
swap pair. - We write an Exchange Contract that will handle the swap functionality. Each
token-near
pair, one exchange contract. - We make a simple
Web UI
to interact with the Exchange Contract, usingnear-sdk-js
andReact
libraries. - We host our DApp on
Skynet
.
MLB1 contract
MLB1 is the symbol of the token we are going to deploy on the NEAR testnet. We will use the official template for the contract, which is based on the NEP-141 standard.
Note, In NEAR each account can only have one smart contract. And re-deploying contracts DOES NOT create a new state.
Building MLB1 contract
git clone https://github.com/near-examples/FT.git MLB1 # cloning the FT template
cd MLB1
./build.sh # Build
Deploying MLB1 contract
From now we consider mlibre.testnet
as the master account.
near create-account mlb1.mlibre.testnet --masterAccount mlibre.testnet # Create a sub account for mlb1 contract
near state mlb1.mlibre.testnet # checking the newly created account state
near deploy --wasmFile res/fungible_token.wasm --accountId mlb1.mlibre.testnet # Deploying the contract
Deploy output is something like this:
Starting deployment. Account id: mlb1.mlibre.testnet, node: https://rpc.testnet.near.org, helper: https://helper.testnet.near.org, file: res/fungible_token.wasm
Transaction Id 8JJCqCXiwVCppQTExUXJBnnXkj4pzvqjNN2jJx5RPeGs
To see the transaction in the transaction explorer, please open this url in your browser
https://explorer.testnet.near.org/transactions/8JJCqCXiwVCppQTExUXJBnnXkj4pzvqjNN2jJx5RPeGs
Done deploying to mlb1.mlibre.testnet
You can check the account in the explorer, there you will find the actions that happened.
Now we should call the new
method of the contract to initialize a token:
near call mlb1.mlibre.testnet new '{"owner_id": "mlb1.mlibre.testnet", "total_supply": "1000000000000000", "metadata": { "spec": "ft-1.0.0", "name": "MLB1", "symbol": "MLB1", "decimals": 8 }}' --accountId mlb1.mlibre.testnet
You can check the explorer for details, or query the account's state with cli:
near state mlb1.mlibre.testnet # Account status
# near view-state mlb1.mlibre.testnet --finality final # key-value status
# near view-state mlb1.mlibre.testnet --finality final --utf8 # key-value status in utf8
# near view mlb1.mlibre.testnet ft_metadata # Token metadata
Output is something like this:
{
amount: '100000262536618865600000010',
block_hash: 'Br6C34jP4B587P7S4fXPocxfMYe85bzU252UH9KXjudf',
block_height: 72853693,
code_hash: '7Mjzf1s65QZ7aeh7xFjfoihrqpinVS7FaNmyh7kqupEN',
locked: '0',
storage_paid_at: 0,
storage_usage: 226967,
formattedAmount: '100.00026253661886560000001'
}
And for MLB1 balance of the account:
near view mlb1.mlibre.testnet ft_balance_of '{"account_id": "mlb1.mlibre.testnet"}'
Exchange contract
The exchange contract takes a token address
parameter in its new
method, which is the token that can be swapped for NEAR and vice versa. Follow the official template to start a new contract.
cargo new exchange
# Follow the doc: https://docs.near.org/docs/develop/contracts/rust/intro
The folder structure looks like this:
├── Cargo.toml
├── src
│ └── lib.rs
└── target
└── exchange.wasm
Contract code is straightforward:
// lib.rs
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, log, near_bindgen, ext_contract, AccountId, Balance, PanicOnDefault, PromiseOrValue};
near_sdk::setup_alloc!();
#[ext_contract(mlb1)]
trait FungibleToken {
fn ft_transfer(&mut self, receiver_id: String, amount: String, memo: Option<String>);
fn ft_total_supply(&self) -> String;
fn ft_balance_of(&self, account_id: String) -> String;
}
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Exchange {
token_address: AccountId,
logo_url: String
}
#[near_bindgen]
impl Exchange {
#[init]
pub fn new(_token_address: AccountId) -> Self {
assert!(!env::state_exists(), "Already initialized");
assert!(&env::signer_account_id() == &env::current_account_id(), "Owner's method");
Self {
token_address: _token_address,
logo_url: "".to_string()
}
}
#[payable]
pub fn near_to_token(&mut self) {
let near = env::attached_deposit();
let account = &env::signer_account_id();
env::log(near.to_string().as_bytes());
env::log(account.to_string().as_bytes());
mlb1::ft_transfer(
account.to_string(),
near.to_string(),
None,
&self.token_address.to_string(), // mlb1 account id
1, // yocto NEAR to attach
5_000_000_000_000 // gas to attach
);
}
pub fn get_token_address(self) -> AccountId {
self.token_address
}
pub fn set_token_address(&mut self, _token_address: AccountId) {
assert!(&env::signer_account_id() == &env::current_account_id(), "Owner's method");
self.token_address = _token_address
}
pub fn set_logo_url(&mut self, url: String) {
assert!(&env::signer_account_id() == &env::current_account_id(), "Owner's method");
self.logo_url = url
}
pub fn get_logo_url(self) -> String {
self.logo_url
}
}
new
function is used to initialize the contract.assert!(&env::signer_account_id() == &env::current_account_id()
is used to check if the current account is the owner of the contract.
near_to_token
function is used to swap NEAR for tokensset_token_address
function is used to set the token address.get_token_address
function is used to get the token address.
And Cargo.toml
is:
[package]
name = "exchange"
version = "1.0.0"
authors = ["Mlibre <m.gh@linuxmail.org>"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
near-sdk = "3.1.0"
[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
As you can see in [dependencies]
section, we are using near-sdk version 3.1.0 rust library.
Deploying the contract
Lets create an account for the exchange contract, compile and deploy it:
# near delete exchange.mlibre.testnet mlibre.testnet
near create-account exchange.mlibre.testnet --masterAccount mlibre.testnet
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/*.wasm ./target
near deploy --wasmFile target/exchange.wasm --accountId exchange.mlibre.testnet
There is a new
method in the exchange contract, we should call it to initialize the token:
near call exchange.mlibre.testnet new '{"_token_address": "mlb1.mlibre.testnet"}' --accountId exchange.mlibre.testnet
# near call exchange.mlibre.testnet set_token_address '{"_token_address": "mlb1.mlibre.testnet"}' --accountId exchange.mlibre.testnet
# near deploy exchange.mlibre.testnet --wasmFile target/exchange.wasm --initFunction 'new' --initArgs '{"_token_address": "mlb1.mlibre.testnet"}'
near view exchange.mlibre.testnet get_token_address --accountId exchange.mlibre.testnet
In order to deposit some MLB1 to the exchange contract, we need to call the storage_deposit
method:
near call mlb1.mlibre.testnet storage_deposit '' --accountId exchange.mlibre.testnet --amount 0.0125
near call mlb1.mlibre.testnet ft_transfer '{"receiver_id": "exchange.mlibre.testnet", "amount": "1900"}' --accountId mlb1.mlibre.testnet --amount 0.000000000000000000000001
near view mlb1.mlibre.testnet ft_balance_of '{"account_id": "mlb1.mlibre.testnet"}'
Swap near to token
Now to swap some NEAR for MLB1 as mlibre.testnet
, we call the near_to_token
method:
near call mlb1.mlibre.testnet storage_deposit '' --accountId mlibre.testnet --amount 0.0125
near call exchange.mlibre.testnet near_to_token --accountId mlibre.testnet --amount 0.000000000000000000000001
near view mlb1.mlibre.testnet ft_balance_of '{"account_id": "exchange.mlibre.testnet"}'
near view mlb1.mlibre.testnet ft_balance_of '{"account_id": "mlibre.testnet"}'
You can find the source codes here
Web UI
Writing a web DApp for the exchange contract is easy. We can use the npx create-near-app web-ui
tool to create a ready-to-use react
, rust
, parcel
stack.
npx create-near-app web-ui --frontend=react --contract=rust
cd web-ui
yarn start # Installing dependencies and starting the app
The folder structure should look like:
|
├── babel.config.js
├── dist
├── package.json
├── src
│ ├── App.js
│ ├── assets
│ │ ├── favicon.ico
│ │ ├── mlogo-black.svg
│ │ └── mlogo-white.svg
│ ├── config.js
│ ├── global.css
│ ├── index.html
│ ├── index.js
│ ├── utils.js
│ └── wallet
│ └── login
│ └── index.html
To connect to a wallet, signing in and out, NEAR provides near-api-js
library.
It is imported in utils.js
.
export async function initContract() {
// Initialize connection to the NEAR testnet
const near = await connect(Object.assign({ deps: { keyStore: new keyStores.BrowserLocalStorageKeyStore() } }, nearConfig))
// Initializing Wallet based Account. It can work with NEAR testnet wallet that
// is hosted at https://wallet.testnet.near.org
window.walletConnection = new WalletConnection(near)
// Getting the Account ID. If still unauthorized, it's just empty string
window.accountId = window.walletConnection.getAccountId()
window.account = await window.walletConnection.account();
// Initializing our contract APIs by contract name and configuration
window.contract = await new Contract(window.account, nearConfig.contractName, {
viewMethods: ['get_token_address'],
changeMethods: ['near_to_token'],
sender: window.account
})
}
Every time the app starts, the initContract
function is called. It initializes the connection to the NEAR testnet and creates an Account
, Wallet
, and Contract
object. Then we call the get_token_address
method to get the token address in App.js
.
export default function App() {
const [token_address, set_token_address] = React.useState()
const [showNotification, setShowNotification] = React.useState(false)
React.useEffect(
async () => {
if (window.walletConnection.isSignedIn()) {
try {
let token_address = await window.contract.get_token_address();
set_token_address(token_address)
} catch (error) {
console.log(error);
}
}
},
[]
)
.
.
.
We also need to change the form action to:
await window.contract.near_to_token({},
300000000000000, // attached GAS (optional)
token_address.value // attached deposit in yoctoNEAR (optional)
)
Before we start the app we are going to make some changes minor changes as well. Start by upgrading the parcel
dependency to the latest version:
Replace
parcel-bundler
withparcel
in thepackage.json
npm i parcel@latest
Add
type="module"
to thescript
tag in thesrc/index.html
file.<script type="module" src="./index.js"></script>
In
config.js
set the contract name toexchange.mlibre.testnet
function getConfig() {
return {
networkId: 'testnet',
nodeUrl: 'https://rpc.testnet.near.org',
contractName: 'exchange.mlibre.testnet',
walletUrl: 'https://wallet.testnet.near.org',
helperUrl: 'https://helper.testnet.near.org',
explorerUrl: 'https://explorer.testnet.near.org',
}
}
You can now run the app using the following command:
parcel src/index.html --open
You can find the source codes here
Hosting On Skynet
After running dapp using parcel src/index.html --open
, copy the dist folder and upload it on Skynet.
References
- https://docs.near.org
- https://github.com/near/near-cli
- https://github.com/mlibre/blockchain/tree/master/NEAR/simple-exchange
About The Author
I'm mlibre, a random guy from the solar galaxy. I am interested in blockchain tech and find it very useful for lots of things. Feel free to check my Github
Donations
ETH:
0xc9b64496986E7b6D4A68fDF69eF132A35e91838e
NEAR:
mlibre.near