Skip to content

Commit

Permalink
near-sdk 5.5.0 and docs (#18)
Browse files Browse the repository at this point in the history
upgrade from near-sdk 4 to 5.5
update README.md files to reflect the current state of the project
add ft_transfer_internal to the JS environment in the FT contract, so that it is possible to transfer tokens without an attached deposit
  • Loading branch information
petersalomonsen authored Oct 15, 2024
1 parent cb5575e commit 9f55fa6
Show file tree
Hide file tree
Showing 18 changed files with 406 additions and 1,032 deletions.
1,112 changes: 189 additions & 923 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ strip="symbols"
strip = false

[dependencies]
near-sdk = "4.0.0"
near-sdk = "5.5.0"
ed25519-dalek = "1.0.1"
sha2 = "0.10.6"
hex = "0.4.3"
Expand Down
71 changes: 23 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,69 +1,44 @@
Rust WebAssembly smart contract for NEAR with Javascript runtime
Rust WebAssembly smart contracts for NEAR with Javascript runtime
================================================================

This is a Proof of Concept of embedding QuickJS with https://github.com/near/near-sdk-rs for being able to execute custom JavaScript code inside a smart contract written in Rust.
This project shows compiling and embedding [QuickJS](https://bellard.org/quickjs/) with https://github.com/near/near-sdk-rs for being able to execute custom JavaScript code inside a smart contract written in Rust. It contains examples of standard contracts like NFT and Fungible token, with JavaScript customization layers on top. There are also examples of a [web4](https://github.com/vgrichina/web4) contract.

First of all, have a look at the videos where I present the project
Check out the youtube playlist with videos showing the project:

https://www.youtube.com/watch?v=JBZEr__pid0&list=PLv5wm4YuO4IwVNrSsYxeqKrtQZYRML03Z

The QuickJS runtime is compiled from https://github.com/petersalomonsen/quickjs-wasm-near
Also check out the [end-to-end](#end-to-end-tests-using-near-workspaces) tests for how to use the contracts from this project.

The contract has two functions:
- `run_script` accepting javascript as text for compiling on the fly.
- `run_bytecode` for running JS pre-compiled into the QuickJS bytecode format. Send the pre-compiled bytecode as a base64 string. See https://github.com/petersalomonsen/quickjs-wasm-near/blob/master/web/compiler/compile.spec.js for examples on compiling JS to QuickJS bytecode.
- `submit_script` for submitting and storing JavaScript and running later
- `run_script_for_account` run script stored by account, returns an integer returned by the script
- `run_script_for_account_no_return` run script stored by account, does not return anything unless the script calls `env.value_return`.
# Devcontainer / github actions

For building and deploying the contract have a look at [buildanddeploy.sh](./buildanddeploy.sh).
All the pre-requisities for getting the project up and running can be found in the [.devcontainer](./.devcontainer) folder, which will be automaticall set up if using a github codespace.

# Calling the deployed contract
The github actions also shows how to build and run all the examples.

Test running javascript as text:
# Architecture / structure

```
near call dev-1650299983789-21350249865305 --accountId=psalomo.testnet run_script '{"script": "(function() {return 5*33+22;})();" }'
```
QuickJS is built with [Emscripten](https://emscripten.org/) to a static library. Another C library, which can be found in the [quickjslib](./quickjslib/) folder, is providing a simplified interface to QuickJS, which is then linked to the Rust code along with other relevant static libraries from the Emscripten distribution ( such as the C standard library, allocator, WASI etc. ).

Here are some examples from a deployment to testnet account: `dev-1650299983789-21350249865305`
See the entire build process in [build.rs](./build.rs).

Test running bytecode ( which is compiled from `JSON.parse('{"a": 222}').a+3`):
In the Rust part, there are contract implementations exposing functions for submitting JavaScript code. Both in the internal bytecode format of QuickJS, and pure JS source code.

```
near call dev-1650299983789-21350249865305 --accountId=psalomo.testnet run_bytecode '{"bytecodebase64": "AgQKcGFyc2UUeyJhIjogMjIyfQJhGDxldmFsc291cmNlPg4ABgCgAQABAAMAABsBogEAAAA4mwAAAELeAAAABN8AAAAkAQBB4AAAALidzSjCAwEA" }'
```
# Testing
# Unit tests running in WebAssembly

Since we are linking with C libraries it is more practical to have Wasm pre-builds and run tests in a Wasm target rather than having builds for native platforms. Run the test using wasi like this:
While it's common and more straightforward for NEAR smart contracts and many other Rust WebAssembly projects, to have their unit tests compiled to the native platform, this project runs the unit test in a WebAssembly runtime. The reason for this is because of the static libraries compiled from C, which are already targeting Wasm. One limitation when running tests inside the Wasm runtime is that you cannot catch panics, and so testing the error messages has to be done in the end-2-end tests

`RUSTFLAGS='-C link-args=--initial-memory=67108864' cargo wasi test -- --show-output --nocapture`
# End-to-end tests using near-workspaces

Unfortunately testing with wasi has some limitations today. Especially panic does not support unwinding in Wasm, and so tests that should panic needs to be performed in e2e test scenarios. Read more here: https://bytecodealliance.github.io/cargo-wasi/testing.html
In the [e2e](./e2e/) folder and also within the [examples](./examples/) folders there are test files that demonstrates deployment and interaction with the contract using [near-workspaces-js](https://github.com/near/near-workspaces-js). All these tests are being run as part of the github actions pipeline, but you can also look at this for examples on how to use the contracts produced in this project.

# Web4 and a WebAssembly Music showcase
# Local JS test environment

The web application in the [web4](./web4) folder is a vanilla JS Web Component application for uploading music written in Javascript and also playing it, and accepting parameters in JSON for configuring the playback. It also contains functionality for exporting to WAV. See the video playlist above for a demo.

The music to be played back is fetched in a view method call, and for controlling who can access this view method the JSON parameters payload is signed using the callers private key. The contract will then verify the signature according to the callers public key stored in a transaction before the view method call.

The web application is packaged into a single HTML file using rollup, where the final bundle is embedded into the Rust sources encoded as a base64 string.

# TODO

- **DONE** Implement (mock) WASI methods in a linkable library so that WAT file does not have to be edited manually
- **DONE** Integration/Unit testing support for Wasm32 target ( which is not supported with near-sdk-rs, see https://github.com/near/near-sdk-rs/issues/467 )
- **DONE** Running tests
- **DONE** Displaying errors (needs a panic hook)
- **DONE** Minimum NEAR mock env
- Local simulation in browser/node Wasm runtime with mocked NEAR env in JavaScript
- **DONE** End to End tests (testnet)
- **DONE** Expose some NEAR environment functions to JS runtime
- **DONE** `env.value_return`
- **DONE** `env.input` (no need to load into register first)
- **DONE** `env.signer_account_id` (no need to load into register first)
- **DONE** Web4 hosting showcase
- **DONE** NFT implementation configurable with JavaScript
- Fungible Token example
A simple mocking of NEAR interfaces for simulation of a smart contract directly in NodeJS or in the browser can be found in [localjstestenv](./localjstestenv/README.md).

# Example contracts

- [NFT](./examples/nft/README.md) - The standard NFT contract, customizable with JavaScript
- [Fungible Token](./examples/fungibletoken/README.md) - The standard FT contract, customizable with JavaScript
- [Minimum Web4](./examples/minimumweb4/README.md) - Implement the web4 interface in JavaScript to serve a website from the smart contract
- "[PureJS](./examples/purejs/README.md)" - Precompile the JS bytecode into the contract, and provide direct exports to the JS functions.
- [Web4 and a WebAssembly Music showcase](./web4/README.md) - JavaScript from WebAssembly Music running in the smart contract
4 changes: 2 additions & 2 deletions examples/fungibletoken/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
near-sdk = "4.0.0"
near-sdk = "5.5.0"
lazy_static = "1.4.0"
ed25519-dalek = "1.0.1"
quickjs-rust-near = { path = "../../", features=["library"] }
quickjs-rust-near-testenv = { path = "../../testenv" }
near-contract-standards = "4.1.1"
near-contract-standards = "5.5.0"

[build-dependencies]
bindgen = "0.60.1"
114 changes: 98 additions & 16 deletions examples/fungibletoken/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ 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::storage_management::{StorageBalance, StorageBalanceBounds};
use near_sdk::borsh::{BorshDeserialize, 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::near;
use near_sdk::{env, log, near_bindgen, AccountId, NearToken, PanicOnDefault, PromiseOrValue};
use quickjs_rust_near::jslib::{
add_function_to_js, arg_to_str, compile_js, js_call_function, load_js_bytecode, to_js_string,
};
Expand All @@ -33,6 +35,7 @@ const JS_BYTECODE_STORAGE_KEY: &[u8] = b"JS";

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
#[borsh(crate = "near_sdk::borsh")]
pub struct Contract {
token: FungibleToken,
metadata: LazyOption<FungibleTokenMetadata>,
Expand Down Expand Up @@ -78,7 +81,7 @@ impl Contract {
this.token.internal_deposit(&owner_id, total_supply.into());
near_contract_standards::fungible_token::events::FtMint {
owner_id: &owner_id,
amount: &total_supply,
amount: total_supply,
memo: Some("Initial tokens supply is minted"),
}
.emit();
Expand Down Expand Up @@ -134,9 +137,8 @@ impl Contract {
add_function_to_js(
"ft_balance_of",
move |ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let balance = (*CONTRACT_REF)
.ft_balance_of(AccountId::new_unchecked(arg_to_str(ctx, 0, argv)))
.0;
let account_id = arg_to_str(ctx, 0, argv).parse().unwrap();
let balance = (*CONTRACT_REF).ft_balance_of(account_id).0;
return to_js_string(ctx, balance.to_string());
},
1,
Expand All @@ -145,13 +147,27 @@ impl Contract {
add_function_to_js(
"ft_transfer",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let receiver_id = AccountId::new_unchecked(arg_to_str(ctx, 0, argv));
let receiver_id = arg_to_str(ctx, 0, argv).parse().unwrap();
let amount: U128 = U128(arg_to_str(ctx, 1, argv).parse::<u128>().unwrap());
(*CONTRACT_REF).ft_transfer(receiver_id, amount, None);
return 0;
},
2,
);

add_function_to_js(
"ft_transfer_internal",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let sender_id = arg_to_str(ctx, 0, argv).parse().unwrap();
let receiver_id = arg_to_str(ctx, 1, argv).parse().unwrap();
let amount: U128 = U128(arg_to_str(ctx, 2, argv).parse::<u128>().unwrap());
(*CONTRACT_REF)
.token
.internal_transfer(&sender_id, &receiver_id, amount.0, None);
return 0;
},
2,
);
}

#[payable]
Expand All @@ -172,17 +188,52 @@ impl Contract {
self.store_js_bytecode(compile_js(javascript, Some("main.js".to_string())));
}

fn on_account_closed(&mut self, account_id: AccountId, balance: Balance) {
fn on_account_closed(&mut self, account_id: AccountId, balance: u128) {
log!("Closed @{} with {}", account_id, balance);
}

fn on_tokens_burned(&mut self, account_id: AccountId, amount: Balance) {
fn on_tokens_burned(&mut self, account_id: AccountId, amount: u128) {
log!("Account @{} burned {}", account_id, amount);
}
}

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 near_contract_standards::storage_management::StorageManagement for Contract {
#[payable]
fn storage_deposit(
&mut self,
account_id: Option<AccountId>,
registration_only: Option<bool>,
) -> StorageBalance {
self.token.storage_deposit(account_id, registration_only)
}

#[payable]
fn storage_withdraw(&mut self, amount: Option<NearToken>) -> StorageBalance {
self.token.storage_withdraw(amount)
}

#[payable]
fn storage_unregister(&mut self, force: Option<bool>) -> bool {
#[allow(unused_variables)]
if let Some((account_id, balance)) = self.token.internal_storage_unregister(force) {
self.on_account_closed(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<StorageBalance> {
self.token.storage_balance_of(account_id)
}
}

#[near_bindgen]
impl FungibleTokenMetadataProvider for Contract {
Expand All @@ -193,16 +244,15 @@ impl FungibleTokenMetadataProvider for Contract {

#[cfg(test)]
mod tests {
use near_sdk::Balance;

use super::*;
use near_contract_standards::storage_management::StorageManagement;
use quickjs_rust_near_testenv::testenv::{
alice, assert_latest_return_value_string_eq, bob, set_attached_deposit,
set_block_timestamp, set_current_account_id, set_input, set_predecessor_account_id,
setup_test_env,
};

const TOTAL_SUPPLY: Balance = 1_000_000_000_000_000;
const TOTAL_SUPPLY: u128 = 1_000_000_000_000_000;

#[test]
fn test_new() {
Expand All @@ -225,7 +275,7 @@ mod tests {
contract.storage_deposit(Some(alice()), Some(true));

set_predecessor_account_id(bob());
set_attached_deposit(1);
set_attached_deposit(NearToken::from_yoctonear(1));
let transfer_amount = TOTAL_SUPPLY / 3;
contract.ft_transfer(alice(), transfer_amount.into(), None);

Expand Down Expand Up @@ -300,7 +350,7 @@ mod tests {
contract.storage_deposit(Some(alice()), Some(true));

set_predecessor_account_id(bob());
set_attached_deposit(1);
set_attached_deposit(NearToken::from_yoctonear(1));

contract.call_js_func("transfer_2_000_to_alice".to_string());
assert_eq!(contract.ft_balance_of(bob()).0, TOTAL_SUPPLY - 2_000);
Expand Down Expand Up @@ -341,7 +391,7 @@ mod tests {
contract.storage_deposit(Some(alice()), Some(true));

set_predecessor_account_id(bob());
set_attached_deposit(1);
set_attached_deposit(NearToken::from_yoctonear(1));

set_block_timestamp(1234_000_000);
contract.call_js_func("transfer_2_000_to_alice".to_string());
Expand All @@ -359,4 +409,36 @@ mod tests {
assert_eq!(contract.ft_balance_of(bob()).0, TOTAL_SUPPLY - 1_000);
assert_eq!(contract.ft_balance_of(alice()).0, 1_000);
}

#[test]
fn test_js_transfer_internal_without_attached_neartokens() {
setup_test_env();

let mut contract = Contract::new_default_meta(bob().into(), TOTAL_SUPPLY.into());
set_current_account_id(bob());
set_predecessor_account_id(bob());
contract.post_javascript(
"
export function transfer_2_000_from_bob_to_alice() {
const amount = 2_000n;
env.ft_transfer_internal('bob.near','alice.near', amount.toString());
env.value_return(transfer_id);
}"
.to_string(),
);

set_predecessor_account_id(alice());
set_attached_deposit(contract.storage_balance_bounds().min.into());

// Paying for account registration, aka storage deposit
contract.storage_deposit(Some(alice()), Some(true));

set_predecessor_account_id(bob());
set_attached_deposit(NearToken::from_yoctonear(0));

set_block_timestamp(1234_000_000);
contract.call_js_func("transfer_2_000_from_bob_to_alice".to_string());
assert_eq!(contract.ft_balance_of(bob()).0, TOTAL_SUPPLY - 2_000);
assert_eq!(contract.ft_balance_of(alice()).0, 2_000);
}
}
2 changes: 1 addition & 1 deletion examples/minimumweb4/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
near-sdk = "4.1.1"
near-sdk = "5.5.0"
quickjs-rust-near = { path = "../../", features=["library"] }
quickjs-rust-near-testenv = { path = "../../testenv" }

Expand Down
23 changes: 23 additions & 0 deletions examples/minimumweb4/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Web4
====

This contract allows you to create a [web4](https://github.com/vgrichina/web4) contract where you can implement the Web4 interfaces in Javascript.

Example:

```javascript
export function web4_get() {
const request = JSON.parse(env.input()).request;

let response;

if (request.path == '/index.html') {
response = {
contentType: "text/html; charset=UTF-8",
body: env.base64_encode("Hello")
};
}
env.value_return(JSON.stringify(response));
}
```

1 change: 1 addition & 0 deletions examples/minimumweb4/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const JS_CONTENT_RESOURCE_PREFIX: &str = "JSC_";

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, Default)]
#[borsh(crate="near_sdk::borsh")]
pub struct Contract {}

static mut CONTRACT_REF: *const Contract = 0 as *const Contract;
Expand Down
4 changes: 2 additions & 2 deletions examples/nft/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
near-sdk = "4.0.0"
near-sdk = "5.5.0"
lazy_static = "1.4.0"
ed25519-dalek = "1.0.1"
quickjs-rust-near = { path = "../../", features=["library"] }
quickjs-rust-near-testenv = { path = "../../testenv" }
near-contract-standards = "4.1.1"
near-contract-standards = "5.5.0"

[build-dependencies]
bindgen = "0.60.1"
Loading

0 comments on commit 9f55fa6

Please sign in to comment.