├── .github ├── codecov.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── dispatch_examples └── greeter │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── docs └── zokyo-audit-report-FRC0046.pdf ├── frc42_dispatch ├── Cargo.toml ├── README.md ├── generate_hashes.py ├── hasher │ ├── Cargo.toml │ └── src │ │ ├── hash.rs │ │ └── lib.rs ├── macros │ ├── Cargo.toml │ ├── example │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── src │ │ ├── hash.rs │ │ └── lib.rs │ └── tests │ │ ├── build-success.rs │ │ └── naming │ │ ├── empty-name-string.rs │ │ ├── empty-name-string.stderr │ │ ├── illegal-chars.rs │ │ ├── illegal-chars.stderr │ │ ├── missing-name.rs │ │ ├── missing-name.stderr │ │ ├── non-capital-start.rs │ │ └── non-capital-start.stderr └── src │ ├── lib.rs │ ├── match_method.rs │ └── message.rs ├── frc46_token ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── receiver.rs │ └── token │ ├── error.rs │ ├── mod.rs │ ├── state.rs │ └── types.rs ├── frc53_nft ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── receiver.rs │ ├── state.rs │ ├── types.rs │ └── util.rs ├── fvm_actor_utils ├── Cargo.toml ├── README.md └── src │ ├── actor.rs │ ├── blockstore.rs │ ├── lib.rs │ ├── messaging.rs │ ├── receiver │ └── mod.rs │ ├── shared_blockstore.rs │ ├── syscalls │ ├── fake_syscalls.rs │ ├── fvm_syscalls.rs │ └── mod.rs │ └── util.rs ├── fvm_dispatch_tools ├── Cargo.toml └── src │ ├── blake2b.rs │ └── main.rs ├── rust-toolchain.toml ├── rustfmt.toml └── testing ├── integration ├── Cargo.toml └── tests │ ├── common │ ├── frc46_token_helpers.rs │ ├── frc53_nft_helpers.rs │ └── mod.rs │ ├── frc46_multi_actor_tests.rs │ ├── frc46_single_actor_tests.rs │ ├── frc46_tokens.rs │ ├── frc53_enumeration.rs │ ├── frc53_multi_actor_tests.rs │ ├── frc53_nfts.rs │ └── transfer_tokens.rs └── test_actors ├── Cargo.toml ├── actors ├── basic_nft_actor │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── basic_receiving_actor │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── basic_token_actor │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── util.rs ├── basic_transfer_actor │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── frc46_factory_token │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ └── lib.rs │ └── token_impl │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── frc46_test_actor │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── frc53_test_actor │ ├── Cargo.toml │ └── src │ └── lib.rs ├── build.rs └── src └── lib.rs /.github/codecov.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filecoin-project/actors-utils/93fb37893b88804c9d811fbd6ee2a82f198c1720/.github/codecov.yml -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check-build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Build 20 | run: make check-build 21 | actor-tests: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Test Actors 28 | run: make test-actors 29 | code-coverage: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Installing Cargo llvm-cov 35 | uses: taiki-e/install-action@5651179950649c44da31d346537e20c0534f0f25 36 | with: 37 | tool: cargo-llvm-cov@0.4.5 38 | - name: Running tests with coverage 39 | run: make ci-test-coverage 40 | - name: Upload coverage to Codecov 41 | uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 42 | with: 43 | files: ci-coverage.info 44 | token: ${{ secrets.CODECOV_TOKEN }} 45 | # Codecov is flaky and will randomly fail. We'd rather not have random failures on master. 46 | fail_ci_if_error: false 47 | verbose: true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # IDE specific user-config 13 | .vscode/ 14 | .idea/ 15 | 16 | # Code coverage instrumentation 17 | coverage/ 18 | *.profraw 19 | 20 | testing/bundles/ 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "dispatch_examples/greeter", 5 | "frc42_dispatch", 6 | "frc42_dispatch/hasher", 7 | "frc42_dispatch/macros", 8 | "frc42_dispatch/macros/example", 9 | "frc46_token", 10 | "frc53_nft", 11 | "fvm_actor_utils", 12 | "fvm_dispatch_tools", 13 | "testing/integration", 14 | "testing/test_actors", 15 | "testing/test_actors/actors/*", 16 | "testing/test_actors/actors/frc46_factory_token/token_impl", 17 | ] 18 | 19 | [workspace.dependencies] 20 | blake2b_simd = { version = "1.0.3" } 21 | clap = { version = "4.5.36", features = ["derive"] } 22 | cid = { version = "0.11.1", default-features = false, features = [ 23 | "serde", 24 | ] } 25 | fvm = { version = "~4.7", default-features = false } 26 | fvm_integration_tests = "~4.7" 27 | fvm_ipld_amt = "0.7.4" 28 | fvm_ipld_bitfield = "0.7.2" 29 | fvm_ipld_blockstore = "0.3.1" 30 | fvm_ipld_encoding = "0.5.3" 31 | fvm_ipld_hamt = "0.10.4" 32 | fvm_sdk = "~4.7" 33 | fvm_shared = "~4.7" 34 | serde = { version = "1.0.219", features = ["derive"] } 35 | thiserror = { version = "2.0.12" } 36 | integer-encoding = { version = "4.0.2" } 37 | num-traits = { version = "0.2.19" } 38 | anyhow = { version = "1.0.98" } 39 | multihash-codetable = { version = "0.1.4", default-features = false } 40 | 41 | # internal deps of published packages 42 | frc42_dispatch = { version = "10.0.0", path = "./frc42_dispatch", default-features = false } 43 | fvm_actor_utils = { version = "14.0.0", path = "./fvm_actor_utils" } 44 | 45 | # only consumed by non-published packages 46 | frc53_nft = { path = "./frc53_nft" } 47 | frc46_token = { path = "./frc46_token" } 48 | 49 | [profile.wasm] 50 | inherits = "release" 51 | panic = "abort" 52 | overflow-checks = false 53 | lto = true 54 | opt-level = "z" 55 | strip = true 56 | codegen-units = 1 57 | incremental = false 58 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | WASM_EXCLUSION = \ 2 | --exclude greeter \ 3 | --exclude helix_integration_tests \ 4 | --exclude basic_token_actor \ 5 | --exclude basic_receiving_actor \ 6 | --exclude basic_nft_actor \ 7 | --exclude basic_transfer_actor \ 8 | --exclude frc46_test_actor \ 9 | --exclude frc46_factory_token \ 10 | --exclude frc53_test_actor 11 | 12 | ACTORS_VERSION=v15.0.0 13 | ACTORS_NETWORK=mainnet 14 | ACTORS_BUNDLE_NAME=builtin-actors-${ACTORS_VERSION}-${ACTORS_NETWORK}.car 15 | ACTORS_URL=https://github.com/filecoin-project/builtin-actors/releases/download/${ACTORS_VERSION}/builtin-actors-${ACTORS_NETWORK}.car 16 | 17 | # actors are built to WASM via the helix_test_actors crate and be built individually as standalone 18 | # crates so we exclude the from this convenience target 19 | build: install-toolchain 20 | cargo build --workspace $(WASM_EXCLUSION) 21 | 22 | check: install-toolchain fetch-bundle 23 | cargo fmt --check 24 | cargo clippy --workspace --all-targets -- -D warnings 25 | 26 | check-build: check build 27 | 28 | 29 | testing/bundles/${ACTORS_BUNDLE_NAME}: 30 | curl -fL --create-dirs --output $@ ${ACTORS_URL} 31 | 32 | fetch-bundle: testing/bundles/${ACTORS_BUNDLE_NAME} 33 | ln -sf ${ACTORS_BUNDLE_NAME} testing/bundles/builtin-actors.car 34 | .PHONY: fetch-bundle 35 | 36 | 37 | test-deps: install-toolchain fetch-bundle 38 | .PHONY: test-deps 39 | 40 | # run all tests, this will not work if using RUSTFLAGS="-Zprofile" to generate profile info or coverage reports 41 | # as any WASM targets will fail to build 42 | test: test-deps 43 | cargo test 44 | 45 | # tests excluding actors so we can generate coverage reports during CI build 46 | # WASM targets such as actors do not support this so are excluded 47 | test-coverage: test-deps 48 | CARGO_INCREMENTAL=0 \ 49 | RUSTFLAGS='-Cinstrument-coverage -C codegen-units=1 -C llvm-args=--inline-threshold=0 -C overflow-checks=off' \ 50 | LLVM_PROFILE_FILE='target/coverage/raw/cargo-test-%p-%m.profraw' \ 51 | cargo test --workspace $(WASM_EXCLUSION) 52 | grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html 53 | 54 | # Just run the tests we want coverage of (for CI) 55 | ci-test-coverage: test-deps 56 | rustup component add llvm-tools-preview 57 | cargo llvm-cov --lcov --output-path ci-coverage.info --workspace $(WASM_EXCLUSION) 58 | 59 | # separate actor testing stage to run from CI without coverage support 60 | test-actors: test-deps 61 | cargo test --package greeter --package helix_integration_tests 62 | 63 | install-toolchain: 64 | rustup show active-toolchain || rustup toolchain install 65 | 66 | clean: 67 | cargo clean 68 | find . -name '*.profraw' -delete 69 | rm Cargo.lock 70 | rm -r testing/bundles 71 | 72 | # generate local coverage report in html format using grcov 73 | # install it with `cargo install grcov` 74 | local-coverage: test-coveuage 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filecoin 2 | 3 | [Filecoin](https://filecoin.io) is a decentralized storage network designed to 4 | store humanity's most important information. 5 | 6 | This repo contains utilities and libraries to work with the 7 | [Filecoin Virtual Machine](https://fvm.filecoin.io/) 8 | 9 | [![codecov](https://codecov.io/gh/filecoin-project/actors-utils/graph/badge.svg?token=5I8ddKxkjm)](https://codecov.io/gh/filecoin-project/actors-utils) 10 | 11 | ## Packages 12 | 13 | ### fvm_actor_utils 14 | 15 | A set of utilities to help write testable native actors for the Filecoin Virtual 16 | Machine. Provides abstractions on top of FVM-SDK functionality that can be 17 | shimmed or mocked in unit tests. This includes helpers for: 18 | 19 | - Universal receiver hooks (as defined in 20 | [FRC-0046](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md)) 21 | - IPLD-compatible blockstore 22 | - Messaging and address resolution 23 | 24 | ### frc42_dispatch 25 | 26 | Reference library containing macros for standard method dispatch. A set of CLI 27 | utilities to generate method numbers is also available: 28 | [fvm_dispatch_tools](./fvm_dispatch_tools/) 29 | 30 | | Specification | Reference Implementation | Examples | 31 | | --------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------ | 32 | | [FRC-0042](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md) | [frc42_dispatch](./frc42_dispatch/README.md) | [greeter](./dispatch_examples/greeter/README.md) | 33 | 34 | ### frc46_token 35 | 36 | Reference library for implementing a standard fungible token in native actors 37 | 38 | | Specification | Reference Implementation | Examples | 39 | | --------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | 40 | | [FRC-0046](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md) | [frc46_token](./frc46_token/README.md) | [basic_token](./testing/test_actors/actors/basic_token_actor/README.md) [basic_receiver](./testing/test_actors/actors/basic_receiving_actor/README.md) | 41 | 42 | ### frc53_nft 43 | 44 | Reference library for implementing a standard non-fungible token in native 45 | actors 46 | 47 | | Specification | Reference Implementation | Examples | 48 | | --------------------------------------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | 49 | | [FRC-0053](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0053.md) | [frc53_nft](./frc53_nft/README.md) | [basic_nft](./testing/test_actors/actors/basic_nft_actor/README.md) [basic_receiver](./testing/test_actors/actors/basic_receiving_actor/README.md) | 50 | 51 | ### frc46_factory_token 52 | 53 | A configurable actor that can be used as a factory to create instances of 54 | [FRC-0046](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md)-compatible 55 | tokens, based on [frc46_token](./frc46_token/README.md) and implemented 56 | [here](./testing/test_actors/actors/frc46_factory_token/) 57 | 58 | ## License 59 | 60 | Dual-licensed: [MIT](./LICENSE-MIT), 61 | [Apache Software License v2](./LICENSE-APACHE). 62 | 63 | ## Testing 64 | 65 | The tests require downloading a builtin-actors bundle. Either run the tests with `make test` or run `make test-deps` before running tests manually with cargo. 66 | 67 | You can change the actors version by changing `ACTORS_VERSION` in the `Makefile`. If you want to test with a custom bundle entirely, replace the `testing/bundles/builtin-actors.car` symlink with the custom bundle. Note, however, that running `make test` will revert that change, so you'll want to test with `cargo test` manually. 68 | 69 | For local coverage testing, please install the `grcov` crate. 70 | 71 | Copyright Protocol Labs, Inc, 2022 72 | -------------------------------------------------------------------------------- /dispatch_examples/greeter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "greeter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | frc42_dispatch = { workspace = true } 9 | fvm_ipld_blockstore = { workspace = true } 10 | fvm_ipld_encoding = { workspace = true } 11 | fvm_sdk = { workspace = true } 12 | fvm_shared = { workspace = true } 13 | 14 | 15 | [lib] 16 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 17 | -------------------------------------------------------------------------------- /dispatch_examples/greeter/README.md: -------------------------------------------------------------------------------- 1 | # Greeter example 2 | A very basic "greeter" actor and an integration test to run it locally. Implements a `Constructor` and a single `Greet` method that takes a string containing a name and returns a greeting. 3 | 4 | ## To run 5 | `cargo build` to build the actor code 6 | `cargo test` to run it in an integration test (using `fvm_integration_tests`) 7 | 8 | Run with `cargo test -- --nocapture` to see the greeting output -------------------------------------------------------------------------------- /dispatch_examples/greeter/src/lib.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::match_method; 2 | use fvm_ipld_encoding::{RawBytes, DAG_CBOR}; 3 | use fvm_sdk as sdk; 4 | use fvm_shared::error::ExitCode; 5 | use sdk::NO_DATA_BLOCK_ID; 6 | 7 | fn greet(name: &str) -> String { 8 | String::from("Hello, ") + name 9 | } 10 | 11 | #[no_mangle] 12 | fn invoke(input: u32) -> u32 { 13 | let method_num = sdk::message::method_number(); 14 | match_method!(method_num, { 15 | "Constructor" => { 16 | // this is a stateless actor so constructor does nothing 17 | NO_DATA_BLOCK_ID 18 | }, 19 | "Greet" => { 20 | // Greet takes a name as a utf8 string 21 | // returns "Hello, {name}" 22 | let params = sdk::message::params_raw(input).unwrap().unwrap(); 23 | let params = RawBytes::new(params.data); 24 | let name = params.deserialize::().unwrap(); 25 | 26 | let greeting = greet(&name); 27 | 28 | let bytes = fvm_ipld_encoding::to_vec(&greeting).unwrap(); 29 | sdk::ipld::put_block(DAG_CBOR, bytes.as_slice()).unwrap() 30 | }, 31 | _ => { 32 | sdk::vm::abort( 33 | ExitCode::USR_ILLEGAL_ARGUMENT.value(), 34 | Some("Unknown method number"), 35 | ); 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /docs/zokyo-audit-report-FRC0046.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filecoin-project/actors-utils/93fb37893b88804c9d811fbd6ee2a82f198c1720/docs/zokyo-audit-report-FRC0046.pdf -------------------------------------------------------------------------------- /frc42_dispatch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frc42_dispatch" 3 | description = "Filecoin FRC-0042 calling convention/dispatch support library" 4 | version = "10.0.0" 5 | license = "MIT OR Apache-2.0" 6 | keywords = ["filecoin", "dispatch", "frc-0042"] 7 | repository = "https://github.com/filecoin-project/actors-utils" 8 | edition = "2021" 9 | 10 | 11 | [dependencies] 12 | fvm_ipld_encoding = { workspace = true } 13 | fvm_sdk = { workspace = true, optional = true } 14 | fvm_shared = { workspace = true } 15 | frc42_hasher = { version = "8.0.0", path = "hasher" } 16 | frc42_macros = { version = "8.0.0", path = "macros" } 17 | thiserror = { version = "2.0.12" } 18 | 19 | [features] 20 | # disable default features to avoid dependence on fvm_sdk (for proc macro and similar purposes) 21 | default = ["use_sdk"] 22 | use_sdk = ["dep:fvm_sdk"] 23 | -------------------------------------------------------------------------------- /frc42_dispatch/README.md: -------------------------------------------------------------------------------- 1 | # frc42_dispatch 2 | 3 | Helper library to work with [FRC-0042](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md) method hashing 4 | 5 | There's an example of it in use [here](https://github.com/filecoin-project/actors-utils/tree/main/dispatch_examples/greeter) -------------------------------------------------------------------------------- /frc42_dispatch/generate_hashes.py: -------------------------------------------------------------------------------- 1 | from hashlib import blake2b 2 | 3 | # See https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md#method-number-computation 4 | def method_number(name): 5 | name = '1|' + name 6 | hash = blake2b(name.encode('ascii'), digest_size=64) 7 | #print('digest: ' + hash.hexdigest()) 8 | #print(f'{len(hash.digest())} bytes long') 9 | 10 | digest = hash.digest() 11 | while digest: 12 | chunk = digest[:4] 13 | num = int.from_bytes(chunk, byteorder='big') 14 | if num >= 1<<24: 15 | return num 16 | digest = digest[4:] 17 | raise Exception("Method ID could not be determined, please change it") 18 | 19 | 20 | # these are all the method names used in the example token actor 21 | methods = ['Name', 'Symbol', 'TotalSupply', 'BalanceOf', 'Allowance', 'IncreaseAllowance', 22 | 'DecreaseAllowance', 'RevokeAllowance', 'Burn', 'TransferFrom', 'Transfer', 'Mint'] 23 | for method in methods: 24 | num = method_number(method) 25 | #print(f'{num:08x}\t{method}') 26 | # print out Rust code for use in a test 27 | print(f'assert_eq!(method_hash!("{method}"), 0x{num:08x});') -------------------------------------------------------------------------------- /frc42_dispatch/hasher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frc42_hasher" 3 | version = "8.0.0" 4 | license = "MIT OR Apache-2.0" 5 | description = "Filecoin FRC-0042 calling convention method hashing" 6 | repository = "https://github.com/filecoin-project/actors-utils" 7 | edition = "2021" 8 | 9 | [dependencies] 10 | fvm_sdk = { workspace = true, optional = true } 11 | fvm_shared = { workspace = true, optional = true } 12 | thiserror = { version = "2.0.12" } 13 | 14 | [features] 15 | # The fvm dependencies are optional. Useful for proc macro and similar purposes. 16 | default = ["use_sdk"] 17 | use_sdk = ["dep:fvm_sdk", "dep:fvm_shared"] 18 | -------------------------------------------------------------------------------- /frc42_dispatch/hasher/src/hash.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Minimal interface for a hashing function. 4 | /// 5 | /// [`Hasher::hash()`] must return a digest that is at least 4 bytes long so that it can be cast to 6 | /// a [`u32`]. 7 | pub trait Hasher { 8 | /// For an input of bytes return a digest that is at least 4 bytes long. 9 | fn hash(&self, bytes: &[u8]) -> Vec; 10 | } 11 | 12 | /// Hasher that uses the blake2b hash syscall provided by the FVM. 13 | #[cfg(feature = "use_sdk")] 14 | #[derive(Default)] 15 | pub struct Blake2bSyscall {} 16 | 17 | #[cfg(feature = "use_sdk")] 18 | impl Hasher for Blake2bSyscall { 19 | // fvm_sdk dependence can be removed by setting default-features to false 20 | fn hash(&self, bytes: &[u8]) -> Vec { 21 | use fvm_shared::crypto::hash::SupportedHashes; 22 | fvm_sdk::crypto::hash_owned(SupportedHashes::Blake2b512, bytes) 23 | } 24 | } 25 | 26 | /// Uses an underlying hashing function (blake2b by convention) to generate method numbers from 27 | /// method names. 28 | #[derive(Default)] 29 | pub struct MethodResolver { 30 | hasher: T, 31 | } 32 | 33 | #[derive(Error, PartialEq, Eq, Debug)] 34 | pub enum MethodNameErr { 35 | #[error("empty method name provided")] 36 | EmptyString, 37 | #[error("method name does not conform to the FRC-0042 convention {0}")] 38 | IllegalName(#[from] IllegalNameErr), 39 | #[error("unable to calculate method id, choose a another method name")] 40 | IndeterminableId, 41 | } 42 | 43 | #[derive(Error, PartialEq, Eq, Debug)] 44 | pub enum IllegalNameErr { 45 | #[error("method name doesn't start with capital letter or _")] 46 | NotValidStart, 47 | #[error("method name contains letters outside [a-zA-Z0-9_]")] 48 | IllegalCharacters, 49 | } 50 | 51 | impl MethodResolver { 52 | const CONSTRUCTOR_METHOD_NAME: &'static str = "Constructor"; 53 | const CONSTRUCTOR_METHOD_NUMBER: u64 = 1_u64; 54 | const FIRST_METHOD_NUMBER: u64 = 1 << 24; 55 | const DIGEST_CHUNK_LENGTH: usize = 4; 56 | 57 | /// Creates a [`MethodResolver`] with an instance of a hasher (blake2b by convention). 58 | pub fn new(hasher: T) -> Self { 59 | Self { hasher } 60 | } 61 | 62 | /// Generates a standard FRC-0042 compliant method number. 63 | /// 64 | /// The method number is calculated as the first four bytes of `hash(method-name)`. 65 | /// The name `Constructor` is always hashed to 1 and other method names that hash to 66 | /// 0 or 1 are avoided via rejection sampling. 67 | pub fn method_number(&self, method_name: &str) -> Result { 68 | check_method_name(method_name)?; 69 | 70 | if method_name == Self::CONSTRUCTOR_METHOD_NAME { 71 | return Ok(Self::CONSTRUCTOR_METHOD_NUMBER); 72 | } 73 | 74 | let method_name = format!("1|{method_name}"); 75 | let digest = self.hasher.hash(method_name.as_bytes()); 76 | 77 | for chunk in digest.chunks(Self::DIGEST_CHUNK_LENGTH) { 78 | if chunk.len() < Self::DIGEST_CHUNK_LENGTH { 79 | // last chunk may be smaller than 4 bytes 80 | break; 81 | } 82 | 83 | let method_id = as_u32(chunk) as u64; 84 | // Method numbers below FIRST_METHOD_NUMBER are reserved for other use 85 | if method_id >= Self::FIRST_METHOD_NUMBER { 86 | return Ok(method_id); 87 | } 88 | } 89 | 90 | Err(MethodNameErr::IndeterminableId) 91 | } 92 | } 93 | 94 | /// Checks that a method name is valid and compliant with the FRC-0042 standard recommendations. 95 | /// 96 | /// - Only ASCII characters in `[a-zA-Z0-9_]` are allowed. 97 | /// - Starts with a character in `[A-Z_]`. 98 | fn check_method_name(method_name: &str) -> Result<(), MethodNameErr> { 99 | if method_name.is_empty() { 100 | return Err(MethodNameErr::EmptyString); 101 | } 102 | 103 | // Check starts with capital letter 104 | let first_letter = method_name.chars().next().unwrap(); // safe because we checked for empty string 105 | if !(first_letter.is_ascii_uppercase() || first_letter == '_') { 106 | return Err(IllegalNameErr::NotValidStart.into()); 107 | } 108 | 109 | // Check that all characters are legal 110 | if !method_name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { 111 | return Err(IllegalNameErr::IllegalCharacters.into()); 112 | } 113 | 114 | Ok(()) 115 | } 116 | 117 | /// Takes a byte array and interprets it as a u32 number. 118 | /// 119 | /// Using big-endian order interperets the first four bytes to an int. 120 | /// 121 | /// The slice passed to this must be at least length 4. 122 | fn as_u32(bytes: &[u8]) -> u32 { 123 | u32::from_be_bytes(bytes[0..4].try_into().expect("bytes was not at least length 4")) 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | 129 | use super::{Hasher, IllegalNameErr, MethodNameErr, MethodResolver}; 130 | 131 | #[derive(Clone, Copy)] 132 | struct FakeHasher {} 133 | impl Hasher for FakeHasher { 134 | fn hash(&self, bytes: &[u8]) -> Vec { 135 | bytes.to_vec() 136 | } 137 | } 138 | 139 | #[test] 140 | fn constructor_is_1() { 141 | let method_hasher = MethodResolver::new(FakeHasher {}); 142 | assert_eq!(method_hasher.method_number("Constructor").unwrap(), 1); 143 | } 144 | 145 | #[test] 146 | fn normal_method_is_hashed() { 147 | let fake_hasher = FakeHasher {}; 148 | let method_hasher = MethodResolver::new(fake_hasher); 149 | // note that the method hashing prepends each name with "1|" as a domain separator 150 | assert_eq!( 151 | method_hasher.method_number("NormalMethod").unwrap(), 152 | super::as_u32(&fake_hasher.hash(b"1|NormalMethod")) as u64 153 | ); 154 | 155 | assert_eq!( 156 | method_hasher.method_number("NormalMethod2").unwrap(), 157 | super::as_u32(&fake_hasher.hash(b"1|NormalMethod2")) as u64 158 | ); 159 | } 160 | 161 | #[test] 162 | fn disallows_invalid_method_names() { 163 | let method_hasher = MethodResolver::new(FakeHasher {}); 164 | assert_eq!( 165 | method_hasher.method_number("Invalid|Method").unwrap_err(), 166 | MethodNameErr::IllegalName(IllegalNameErr::IllegalCharacters) 167 | ); 168 | assert_eq!(method_hasher.method_number("").unwrap_err(), MethodNameErr::EmptyString); 169 | assert_eq!( 170 | method_hasher.method_number("invalidMethod").unwrap_err(), 171 | MethodNameErr::IllegalName(IllegalNameErr::NotValidStart) 172 | ); 173 | } 174 | 175 | /// Fake hasher that always returns a digest beginning with b"\0\0\0\0". 176 | #[derive(Clone, Copy)] 177 | struct FakeHasher0 {} 178 | impl Hasher for FakeHasher0 { 179 | fn hash(&self, bytes: &[u8]) -> Vec { 180 | let mut hash: Vec = vec![0, 0, 0, 0]; 181 | let mut suffix = bytes.to_vec(); 182 | hash.append(suffix.as_mut()); 183 | hash 184 | } 185 | } 186 | 187 | /// Fake hasher that always returns a digest beginning with b"\0\0\0\1". 188 | #[derive(Clone, Copy)] 189 | struct FakeHasher1 {} 190 | impl Hasher for FakeHasher1 { 191 | fn hash(&self, bytes: &[u8]) -> Vec { 192 | let mut hash: Vec = vec![0, 0, 0, 1]; 193 | let mut suffix = bytes.to_vec(); 194 | hash.append(suffix.as_mut()); 195 | hash 196 | } 197 | } 198 | 199 | #[test] 200 | fn avoids_disallowed_method_numbers() { 201 | let hasher_0 = FakeHasher0 {}; 202 | let method_hasher_0 = MethodResolver::new(hasher_0); 203 | 204 | // This simulates a method name that would hash to 0 205 | let contrived_0 = "MethodName"; 206 | let contrived_0_digest = hasher_0.hash(contrived_0.as_bytes()); 207 | assert_eq!(super::as_u32(&contrived_0_digest), 0); 208 | // But the method number is not a collision 209 | assert_ne!(method_hasher_0.method_number(contrived_0).unwrap(), 0); 210 | 211 | let hasher_1 = FakeHasher1 {}; 212 | let method_hasher_1 = MethodResolver::new(hasher_1); 213 | // This simulates a method name that would hash to 1 214 | let contrived_1 = "MethodName"; 215 | let contrived_1_digest = hasher_1.hash(contrived_1.as_bytes()); 216 | assert_eq!(super::as_u32(&contrived_1_digest), 1); 217 | // But the method number is not a collision 218 | assert_ne!(method_hasher_1.method_number(contrived_1).unwrap(), 1); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /frc42_dispatch/hasher/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod hash; 2 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frc42_macros" 3 | version = "8.0.0" 4 | license = "MIT OR Apache-2.0" 5 | description = "Filecoin FRC-0042 calling convention procedural macros" 6 | repository = "https://github.com/filecoin-project/actors-utils" 7 | edition = "2021" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | blake2b_simd = { version = "1.0.3" } 14 | frc42_hasher = { version = "8.0.0", path = "../hasher", default-features = false } 15 | proc-macro2 = "1.0" 16 | quote = "1.0" 17 | syn = { version = "2.0", features = ["full"] } 18 | 19 | [dev-dependencies] 20 | trybuild = "1.0" 21 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | frc42_macros = { version = "8.0.0", path = ".." } 9 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/example/src/main.rs: -------------------------------------------------------------------------------- 1 | use frc42_macros::method_hash; 2 | 3 | fn main() { 4 | let str_hash = method_hash!("Method"); 5 | println!("String hash: {str_hash:x}"); 6 | 7 | // this one breaks naming rules and will fail to compile 8 | //println!("error hash: {}", method_hash!("some_function")); 9 | } 10 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/src/hash.rs: -------------------------------------------------------------------------------- 1 | use blake2b_simd::blake2b; 2 | use frc42_hasher::hash::Hasher; 3 | 4 | pub struct Blake2bHasher {} 5 | impl Hasher for Blake2bHasher { 6 | fn hash(&self, bytes: &[u8]) -> Vec { 7 | blake2b(bytes).as_bytes().to_vec() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use frc42_hasher::hash::MethodResolver; 2 | use proc_macro::TokenStream; 3 | use quote::quote; 4 | use syn::parse::{Parse, ParseStream}; 5 | use syn::{parse_macro_input, LitStr, Result}; 6 | 7 | mod hash; 8 | use crate::hash::Blake2bHasher; 9 | 10 | struct MethodName(LitStr); 11 | 12 | impl MethodName { 13 | /// Hash the method name. 14 | fn hash(&self) -> u64 { 15 | let resolver = MethodResolver::new(Blake2bHasher {}); 16 | resolver.method_number(&self.0.value()).unwrap() 17 | } 18 | } 19 | 20 | impl Parse for MethodName { 21 | fn parse(input: ParseStream) -> Result { 22 | let lookahead = input.lookahead1(); 23 | 24 | if lookahead.peek(LitStr) { 25 | input.parse().map(MethodName) 26 | } else { 27 | Err(lookahead.error()) 28 | } 29 | } 30 | } 31 | 32 | #[proc_macro] 33 | pub fn method_hash(input: TokenStream) -> TokenStream { 34 | let name: MethodName = parse_macro_input!(input); 35 | let hash = name.hash(); 36 | quote!(#hash).into() 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | #[test] 42 | fn it_works() { 43 | let t = trybuild::TestCases::new(); 44 | t.pass("tests/build-success.rs"); 45 | } 46 | 47 | #[test] 48 | fn empty_names() { 49 | let t = trybuild::TestCases::new(); 50 | // NOTE: these need to live in a separate directory under `tests` 51 | // otherwise cargo tries to build them every time and everything breaks 52 | t.compile_fail("tests/naming/empty-name-string.rs"); 53 | t.compile_fail("tests/naming/missing-name.rs"); 54 | } 55 | 56 | #[test] 57 | fn bad_names() { 58 | let t = trybuild::TestCases::new(); 59 | t.compile_fail("tests/naming/illegal-chars.rs"); 60 | t.compile_fail("tests/naming/non-capital-start.rs"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/build-success.rs: -------------------------------------------------------------------------------- 1 | use frc42_macros::method_hash; 2 | 3 | fn main() { 4 | assert_eq!(method_hash!("Method"), 0xa20642fc); 5 | assert_eq!(method_hash!("_Method"), 0xeb9575aa); 6 | 7 | // method names from the example token actor 8 | // numbers are hashed by the python script included in the main dispatch crate 9 | assert_eq!(method_hash!("Name"), 0x02ea015c); 10 | assert_eq!(method_hash!("Symbol"), 0x7adab63e); 11 | assert_eq!(method_hash!("TotalSupply"), 0x06da7a35); 12 | assert_eq!(method_hash!("BalanceOf"), 0x8710e1ac); 13 | assert_eq!(method_hash!("Allowance"), 0xfaa45236); 14 | assert_eq!(method_hash!("IncreaseAllowance"), 0x69ecb918); 15 | assert_eq!(method_hash!("DecreaseAllowance"), 0x5b286f21); 16 | assert_eq!(method_hash!("RevokeAllowance"), 0xa4d840b1); 17 | assert_eq!(method_hash!("Burn"), 0x5584159a); 18 | assert_eq!(method_hash!("TransferFrom"), 0xd7d4deed); 19 | assert_eq!(method_hash!("Transfer"), 0x04cbf732); 20 | assert_eq!(method_hash!("Mint"), 0x06f84ab2); 21 | } 22 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/empty-name-string.rs: -------------------------------------------------------------------------------- 1 | use frc42_macros::method_hash; 2 | 3 | fn main() { 4 | // this should panic due to empty string 5 | let _str_hash = method_hash!(""); 6 | } 7 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/empty-name-string.stderr: -------------------------------------------------------------------------------- 1 | error: proc macro panicked 2 | --> tests/naming/empty-name-string.rs:5:18 3 | | 4 | 5 | let _str_hash = method_hash!(""); 5 | | ^^^^^^^^^^^^^^^^ 6 | | 7 | = help: message: called `Result::unwrap()` on an `Err` value: EmptyString 8 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/illegal-chars.rs: -------------------------------------------------------------------------------- 1 | use frc42_macros::method_hash; 2 | 3 | fn main() { 4 | // should panic because the name contains illegal chars 5 | let _str_hash = method_hash!("Bad!Method!Name!"); 6 | } 7 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/illegal-chars.stderr: -------------------------------------------------------------------------------- 1 | error: proc macro panicked 2 | --> tests/naming/illegal-chars.rs:5:18 3 | | 4 | 5 | let _str_hash = method_hash!("Bad!Method!Name!"); 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | | 7 | = help: message: called `Result::unwrap()` on an `Err` value: IllegalName(IllegalCharacters) 8 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/missing-name.rs: -------------------------------------------------------------------------------- 1 | use frc42_macros::method_hash; 2 | 3 | fn main() { 4 | // should panic because no string or identifier provided 5 | let _ident_hash = method_hash!(); 6 | } 7 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/missing-name.stderr: -------------------------------------------------------------------------------- 1 | error: unexpected end of input, expected string literal 2 | --> tests/naming/missing-name.rs:5:23 3 | | 4 | 5 | let _ident_hash = method_hash!(); 5 | | ^^^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the macro `method_hash` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/non-capital-start.rs: -------------------------------------------------------------------------------- 1 | use frc42_macros::method_hash; 2 | 3 | fn main() { 4 | // should panic because the name starts with non-capital letter 5 | let _str_hash = method_hash!("noPlaceForCamelCase"); 6 | } 7 | -------------------------------------------------------------------------------- /frc42_dispatch/macros/tests/naming/non-capital-start.stderr: -------------------------------------------------------------------------------- 1 | error: proc macro panicked 2 | --> tests/naming/non-capital-start.rs:5:18 3 | | 4 | 5 | let _str_hash = method_hash!("noPlaceForCamelCase"); 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | | 7 | = help: message: called `Result::unwrap()` on an `Err` value: IllegalName(NotValidStart) 8 | -------------------------------------------------------------------------------- /frc42_dispatch/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use frc42_hasher as hasher; 2 | pub use frc42_hasher::hash; 3 | pub use frc42_macros::method_hash; 4 | 5 | pub mod match_method; 6 | pub mod message; 7 | 8 | #[cfg(test)] 9 | mod tests {} 10 | -------------------------------------------------------------------------------- /frc42_dispatch/src/match_method.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! match_method { 3 | ($method:expr, {$($body:tt)*}) => { 4 | match_method!{@match $method, {}, $($body)*} 5 | }; 6 | (@match $method:expr, {$($body:tt)*}, $(,)*) => { 7 | match $method { 8 | $($body)* 9 | } 10 | }; 11 | // matches block with comma 12 | (@match $method:expr, {$($body:tt)*}, $p:literal => $e:expr, $($tail:tt)*) => { 13 | match_method! { 14 | @match 15 | $method, 16 | { 17 | $($body)* 18 | $crate::method_hash!($p) => $e, 19 | }, 20 | $($tail)* 21 | } 22 | }; 23 | // matches block without comma 24 | (@match $method:expr, {$($body:tt)*}, $p:literal => $e:block $($tail:tt)*) => { 25 | match_method! { 26 | @match 27 | $method, 28 | { 29 | $($body)* 30 | $crate::method_hash!($p) => $e, 31 | }, 32 | $($tail)* 33 | } 34 | }; 35 | // matches _ with a trailing comma 36 | (@match $method:expr, {$($body:tt)*}, _ => $e:expr, $($tail:tt)*) => { 37 | match_method! { 38 | @match 39 | $method, 40 | { 41 | $($body)* 42 | _ => $e, 43 | }, 44 | $($tail)* 45 | } 46 | }; 47 | // matches _ without a trailing comma (common if it's the last item) 48 | (@match $method:expr, {$($body:tt)*}, _ => $e:expr) => { 49 | match_method! { 50 | @match 51 | $method, 52 | { 53 | $($body)* 54 | _ => $e, 55 | }, 56 | } 57 | }; 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | #[test] 63 | fn handle_constructor() { 64 | let method_num = 1u64; // constructor should always hash to 1 65 | let ret = match_method!(method_num, { 66 | "Constructor" => Some(1), 67 | _ => None, 68 | }); 69 | 70 | assert_eq!(ret, Some(1)); 71 | } 72 | 73 | #[test] 74 | fn handle_unknown_method() { 75 | let method_num = 12345u64; // not a method we know about 76 | let ret = match_method!(method_num, { 77 | "Constructor" => Some(1), 78 | _ => None, 79 | }); 80 | 81 | assert_eq!(ret, None); 82 | } 83 | 84 | #[test] 85 | fn handle_user_method() { 86 | let method_num = crate::method_hash!("TokensReceived"); 87 | let ret = match_method!(method_num, { 88 | "Constructor" => Some(1), 89 | "TokensReceived" => Some(2), 90 | _ => None, 91 | }); 92 | 93 | assert_eq!(ret, Some(2)); 94 | } 95 | 96 | #[test] 97 | fn handle_optional_commas() { 98 | let method_num = crate::method_hash!("TokensReceived"); 99 | let ret = match_method!(method_num, { 100 | "Constructor" => Some(1), 101 | "TokensReceived" => { 102 | Some(2) 103 | } 104 | _ => { 105 | None 106 | } 107 | }); 108 | 109 | assert_eq!(ret, Some(2)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /frc42_dispatch/src/message.rs: -------------------------------------------------------------------------------- 1 | use fvm_ipld_encoding::ipld_block::IpldBlock; 2 | #[cfg(feature = "use_sdk")] 3 | use fvm_sdk::send; 4 | use fvm_shared::{address::Address, econ::TokenAmount, error::ErrorNumber, Response}; 5 | use thiserror::Error; 6 | 7 | use crate::hash::{Hasher, MethodNameErr, MethodResolver}; 8 | 9 | /// Utility to invoke standard methods on deployed actors. 10 | #[derive(Default)] 11 | pub struct MethodMessenger { 12 | method_resolver: MethodResolver, 13 | } 14 | 15 | #[derive(Error, PartialEq, Eq, Debug)] 16 | pub enum MethodMessengerError { 17 | #[error("error when calculating method name: `{0}`")] 18 | MethodName(#[from] MethodNameErr), 19 | #[error("error sending message: `{0}`")] 20 | Syscall(#[from] ErrorNumber), 21 | } 22 | 23 | impl MethodMessenger { 24 | /// Creates a new method messenger using a specified hashing function (blake2b by default). 25 | pub fn new(hasher: T) -> Self { 26 | Self { method_resolver: MethodResolver::new(hasher) } 27 | } 28 | 29 | /// Calls a method (by name) on a specified actor by constructing and publishing the underlying 30 | /// on-chain message. 31 | #[cfg(feature = "use_sdk")] 32 | pub fn call_method( 33 | &self, 34 | to: &Address, 35 | method: &str, 36 | params: Option, 37 | value: TokenAmount, 38 | ) -> Result { 39 | let method = self.method_resolver.method_number(method)?; 40 | send::send(to, method, params, value, None, fvm_shared::sys::SendFlags::empty()) 41 | .map_err(MethodMessengerError::from) 42 | } 43 | 44 | #[cfg(not(feature = "use_sdk"))] 45 | #[allow(unused_variables)] 46 | pub fn call_method( 47 | &self, 48 | to: &Address, 49 | method: &str, 50 | params: Option, 51 | value: TokenAmount, 52 | ) -> Result { 53 | let _method = self.method_resolver.method_number(method)?; 54 | unimplemented!() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frc46_token/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frc46_token" 3 | description = "Filecoin FRC-0046 fungible token reference implementation" 4 | version = "14.0.0" 5 | license = "MIT OR Apache-2.0" 6 | keywords = ["filecoin", "fvm", "token", "frc-0046"] 7 | repository = "https://github.com/filecoin-project/actors-utils" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | frc42_dispatch = { workspace = true } 12 | fvm_actor_utils = { workspace = true } 13 | 14 | cid = { workspace = true } 15 | fvm_ipld_blockstore = { workspace = true } 16 | fvm_ipld_hamt = { workspace = true } 17 | fvm_ipld_encoding = { workspace = true } 18 | fvm_sdk = { workspace = true } 19 | fvm_shared = { workspace = true } 20 | multihash-codetable = { workspace = true, features = ["blake2b"] } 21 | num-traits = { workspace = true } 22 | serde = { workspace = true } 23 | thiserror = { workspace = true } 24 | integer-encoding = { workspace = true } 25 | -------------------------------------------------------------------------------- /frc46_token/README.md: -------------------------------------------------------------------------------- 1 | # frc46_token 2 | 3 | This is the reference Rust library that implements the 4 | [FRC-0046 fungible token standard](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md). 5 | It is intended for use in native user-programmable actors deployed to the 6 | Filecoin Virtual Machine. 7 | 8 | ## Security Audit 9 | 10 | Zokyo provided an independent security audit on this reference implementation. 11 | Their findings have been attached [here](../docs/zokyo-audit-report-FRC0046.pdf) 12 | -------------------------------------------------------------------------------- /frc46_token/src/lib.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/filecoin-project/actors-utils/issues/165 2 | pub mod receiver; 3 | pub mod token; 4 | -------------------------------------------------------------------------------- /frc46_token/src/receiver.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::method_hash; 2 | use fvm_actor_utils::receiver::{ReceiverHook, ReceiverHookError, ReceiverType, RecipientData}; 3 | use fvm_ipld_encoding::tuple::*; 4 | use fvm_ipld_encoding::RawBytes; 5 | use fvm_shared::{address::Address, econ::TokenAmount, ActorID}; 6 | 7 | pub const FRC46_TOKEN_TYPE: ReceiverType = method_hash!("FRC46") as u32; 8 | 9 | pub trait FRC46ReceiverHook { 10 | fn new_frc46( 11 | address: Address, 12 | frc46_params: FRC46TokenReceived, 13 | result_data: T, 14 | ) -> std::result::Result, ReceiverHookError>; 15 | } 16 | 17 | impl FRC46ReceiverHook for ReceiverHook { 18 | /// Construct a new FRC46 [`ReceiverHook`] call. 19 | fn new_frc46( 20 | address: Address, 21 | frc46_params: FRC46TokenReceived, 22 | result_data: T, 23 | ) -> std::result::Result, ReceiverHookError> { 24 | Ok(ReceiverHook::new( 25 | address, 26 | RawBytes::serialize(frc46_params)?, 27 | FRC46_TOKEN_TYPE, 28 | result_data, 29 | )) 30 | } 31 | } 32 | 33 | /// Receive parameters for an FRC46 token. 34 | #[derive(Serialize_tuple, Deserialize_tuple, PartialEq, Eq, Clone, Debug)] 35 | pub struct FRC46TokenReceived { 36 | /// The account that the tokens are being pulled from (the token actor address itself for mint). 37 | pub from: ActorID, 38 | /// The account that the tokens are being sent to (the receiver address). 39 | pub to: ActorID, 40 | /// Address of the operator that initiated the transfer/mint. 41 | pub operator: ActorID, 42 | /// Amount of tokens being transferred/minted. 43 | pub amount: TokenAmount, 44 | /// Data specified by the operator during transfer/mint. 45 | pub operator_data: RawBytes, 46 | /// Additional data specified by the token-actor during transfer/mint. 47 | pub token_data: RawBytes, 48 | } 49 | -------------------------------------------------------------------------------- /frc46_token/src/token/error.rs: -------------------------------------------------------------------------------- 1 | use fvm_actor_utils::messaging::MessagingError; 2 | use fvm_actor_utils::receiver::ReceiverHookError; 3 | use fvm_ipld_encoding::Error as SerializationError; 4 | use fvm_shared::address::{Address, Error as AddressError}; 5 | use fvm_shared::econ::TokenAmount; 6 | use fvm_shared::error::ExitCode; 7 | use thiserror::Error; 8 | 9 | use crate::token::state::StateError as TokenStateError; 10 | use crate::token::state::StateInvariantError; 11 | 12 | #[derive(Error, Debug)] 13 | pub enum TokenError { 14 | #[error("error in underlying state {0}")] 15 | TokenState(#[from] TokenStateError), 16 | #[error("value {amount:?} for {name:?} must be non-negative")] 17 | InvalidNegative { name: &'static str, amount: TokenAmount }, 18 | #[error("amount {amount:?} for {name:?} must be a multiple of {granularity:?}")] 19 | InvalidGranularity { name: &'static str, amount: TokenAmount, granularity: u64 }, 20 | #[error("error calling other actor: {0}")] 21 | Messaging(#[from] MessagingError), 22 | #[error("receiver hook error: {0}")] 23 | ReceiverHook(#[from] ReceiverHookError), 24 | #[error("expected {address:?} to be a resolvable id address but threw {source:?} when attempting to resolve")] 25 | InvalidIdAddress { 26 | address: Address, 27 | #[source] 28 | source: AddressError, 29 | }, 30 | #[error("operator cannot be the same as the debited address {0}")] 31 | InvalidOperator(Address), 32 | #[error("error during serialization {0}")] 33 | Serialization(#[from] SerializationError), 34 | #[error("error in state invariants {0}")] 35 | StateInvariant(#[from] StateInvariantError), 36 | } 37 | 38 | impl From<&TokenError> for ExitCode { 39 | fn from(error: &TokenError) -> Self { 40 | match error { 41 | TokenError::InvalidIdAddress { address: _, source: _ } => ExitCode::USR_NOT_FOUND, 42 | TokenError::Serialization(_) => ExitCode::USR_SERIALIZATION, 43 | TokenError::InvalidOperator(_) 44 | | TokenError::InvalidGranularity { name: _, amount: _, granularity: _ } 45 | | TokenError::InvalidNegative { name: _, amount: _ } => ExitCode::USR_ILLEGAL_ARGUMENT, 46 | TokenError::StateInvariant(_) => ExitCode::USR_ILLEGAL_STATE, 47 | TokenError::TokenState(state_error) => state_error.into(), 48 | TokenError::ReceiverHook(e) => e.into(), 49 | TokenError::Messaging(messaging_error) => messaging_error.into(), 50 | } 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod test { 56 | use fvm_actor_utils::{messaging::MessagingError, receiver::ReceiverHookError}; 57 | use fvm_ipld_encoding::{CodecProtocol, Error as SerializationError}; 58 | use fvm_shared::{ 59 | address::{Address, Error as AddressError}, 60 | econ::TokenAmount, 61 | error::ExitCode, 62 | }; 63 | 64 | use crate::token::{state::StateInvariantError, TokenError, TokenStateError}; 65 | 66 | #[test] 67 | fn it_creates_exit_codes() { 68 | let error = TokenError::TokenState(TokenStateError::MissingState(cid::Cid::default())); 69 | let msg = error.to_string(); 70 | let exit_code = ExitCode::from(&error); 71 | // taking the exit code doesn't consume the error 72 | println!("{msg}: {exit_code:?}"); 73 | assert_eq!(exit_code, ExitCode::USR_ILLEGAL_STATE); 74 | } 75 | 76 | #[test] 77 | fn error_codes_and_messages() { 78 | // check the exit codes from all the other error types 79 | let err = TokenError::InvalidIdAddress { 80 | address: Address::new_id(1), 81 | source: AddressError::UnknownNetwork, 82 | }; 83 | assert_eq!(ExitCode::USR_NOT_FOUND, ExitCode::from(&err)); 84 | assert_eq!(err.to_string(), String::from("expected Address(\"f01\") to be a resolvable id address but threw UnknownNetwork when attempting to resolve")); 85 | 86 | let err = TokenError::InvalidOperator(Address::new_id(1)); 87 | assert_eq!(ExitCode::USR_ILLEGAL_ARGUMENT, ExitCode::from(&err)); 88 | assert_eq!( 89 | err.to_string(), 90 | String::from("operator cannot be the same as the debited address f01") 91 | ); 92 | 93 | let err = TokenError::InvalidGranularity { 94 | name: "test", 95 | amount: TokenAmount::from_whole(1), 96 | granularity: 10, 97 | }; 98 | assert_eq!(ExitCode::USR_ILLEGAL_ARGUMENT, ExitCode::from(&err)); 99 | assert_eq!( 100 | err.to_string(), 101 | String::from("amount TokenAmount(1.0) for \"test\" must be a multiple of 10") 102 | ); 103 | 104 | let err = TokenError::InvalidNegative { name: "test", amount: TokenAmount::from_whole(-1) }; 105 | assert_eq!(ExitCode::USR_ILLEGAL_ARGUMENT, ExitCode::from(&err)); 106 | assert_eq!( 107 | err.to_string(), 108 | String::from("value TokenAmount(-1.0) for \"test\" must be non-negative") 109 | ); 110 | 111 | let err = TokenError::StateInvariant(StateInvariantError::SupplyNegative( 112 | TokenAmount::from_whole(-1), 113 | )); 114 | assert_eq!(ExitCode::USR_ILLEGAL_STATE, ExitCode::from(&err)); 115 | assert_eq!( 116 | err.to_string(), 117 | String::from("error in state invariants total supply was negative: -1.0") 118 | ); 119 | 120 | let err = TokenError::Serialization(SerializationError { 121 | description: "test".into(), 122 | protocol: CodecProtocol::Cbor, 123 | }); 124 | assert_eq!(ExitCode::USR_SERIALIZATION, ExitCode::from(&err)); 125 | assert_eq!( 126 | err.to_string(), 127 | String::from("error during serialization Serialization error for Cbor protocol: test") 128 | ); 129 | 130 | let err = TokenError::Messaging(MessagingError::AddressNotResolved(Address::new_id(1))); 131 | // error code comes from MessagingError 132 | assert_eq!(ExitCode::USR_NOT_FOUND, ExitCode::from(&err)); 133 | assert_eq!( 134 | err.to_string(), 135 | String::from("error calling other actor: address could not be resolved: `f01`") 136 | ); 137 | 138 | let err = TokenError::ReceiverHook(ReceiverHookError::NotCalled); 139 | // error code comes from ReceiverHookError 140 | assert_eq!(ExitCode::USR_ASSERTION_FAILED, ExitCode::from(&err)); 141 | assert_eq!( 142 | err.to_string(), 143 | String::from("receiver hook error: receiver hook was not called") 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /frc46_token/src/token/types.rs: -------------------------------------------------------------------------------- 1 | use fvm_actor_utils::receiver::RecipientData; 2 | use fvm_ipld_encoding::tuple::*; 3 | use fvm_ipld_encoding::RawBytes; 4 | use fvm_shared::address::Address; 5 | use fvm_shared::econ::TokenAmount; 6 | 7 | /// A standard fungible token interface allowing for on-chain transactions that implements the 8 | /// FRC-0046 standard. This represents the external interface exposed to other on-chain actors. 9 | /// 10 | /// Token authors must implement this trait and link the methods to standard dispatch numbers (as 11 | /// defined by [FRC-0042](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md)). 12 | pub trait FRC46Token { 13 | type TokenError; 14 | /// Returns the name of the token. 15 | /// 16 | /// Must not be empty. 17 | fn name(&self) -> String; 18 | 19 | /// Returns the ticker symbol of the token. 20 | /// 21 | /// Must not be empty. Should be a short uppercase string. 22 | fn symbol(&self) -> String; 23 | 24 | /// Returns the smallest amount of tokens which is indivisible. 25 | /// 26 | /// All transfers, burns, and mints must be a whole multiple of the granularity. All balances 27 | /// must be a multiple of this granularity (but allowances need not be). Must be at least 1. 28 | /// Must never change. 29 | /// 30 | /// A granularity of 10^18 corresponds to whole units only, with no further decimal precision. 31 | fn granularity(&self) -> GranularityReturn; 32 | 33 | /// Returns the total amount of the token in existence. 34 | /// 35 | /// Must be non-negative. The total supply must equal the balances of all addresses. The total 36 | /// supply should equal the sum of all minted tokens less the sum of all burnt tokens. 37 | fn total_supply(&mut self) -> TotalSupplyReturn; 38 | 39 | /// Returns the balance of an address. 40 | /// 41 | /// Balance is always non-negative. Uninitialised addresses have an implicit zero balance. 42 | fn balance_of(&mut self, params: Address) -> Result; 43 | 44 | /// Returns the allowance approved for an operator on a spender's balance. 45 | /// 46 | /// The operator can burn or transfer the allowance amount out of the owner's address. 47 | fn allowance( 48 | &mut self, 49 | params: GetAllowanceParams, 50 | ) -> Result; 51 | 52 | /// Transfers tokens from the caller to another address. 53 | /// 54 | /// Amount must be non-negative (but can be zero). Transferring to the caller's own address must 55 | /// be treated as a normal transfer. Must call the receiver hook on the receiver's address, 56 | /// failing and aborting the transfer if calling the hook fails or aborts. 57 | fn transfer(&mut self, params: TransferParams) -> Result; 58 | 59 | /// Transfers tokens from one address to another. 60 | /// 61 | /// The caller must have previously approved to control at least the sent amount. If successful, 62 | /// the amount transferred is deducted from the caller's allowance. 63 | fn transfer_from( 64 | &mut self, 65 | params: TransferFromParams, 66 | ) -> Result; 67 | 68 | /// Atomically increases the approved allowance that a operator can transfer/burn from the 69 | /// caller's balance. 70 | /// 71 | /// The increase must be non-negative. Returns the new total allowance approved for that 72 | /// owner-operator pair. 73 | fn increase_allowance( 74 | &mut self, 75 | params: IncreaseAllowanceParams, 76 | ) -> Result; 77 | 78 | /// Atomically decreases the approved balance that a operator can transfer/burn from the 79 | /// caller's balance. 80 | /// 81 | /// The decrease must be non-negative. Sets the allowance to zero if the decrease is greater 82 | /// than the currently approved allowance. Returns the new total allowance approved for that 83 | /// owner-operator pair. 84 | fn decrease_allowance( 85 | &mut self, 86 | params: DecreaseAllowanceParams, 87 | ) -> Result; 88 | 89 | /// Sets the allowance a operator has on the owner's account to zero. 90 | fn revoke_allowance( 91 | &mut self, 92 | params: RevokeAllowanceParams, 93 | ) -> Result; 94 | 95 | /// Burns tokens from the caller's balance, decreasing the total supply. 96 | fn burn(&mut self, params: BurnParams) -> Result; 97 | 98 | /// Burns tokens from an address's balance. 99 | /// 100 | /// The caller must have been previously approved to control at least the burnt amount. 101 | fn burn_from(&mut self, params: BurnFromParams) -> Result; 102 | } 103 | 104 | pub type GranularityReturn = u64; 105 | pub type TotalSupplyReturn = TokenAmount; 106 | pub type BalanceReturn = TokenAmount; 107 | pub type AllowanceReturn = TokenAmount; 108 | pub type IncreaseAllowanceReturn = TokenAmount; 109 | pub type DecreaseAllowanceReturn = TokenAmount; 110 | pub type RevokeAllowanceReturn = (); 111 | 112 | /// Return value after a successful mint. 113 | /// 114 | /// The mint method is not standardised, so this is merely a useful library-level type, 115 | /// and recommendation for token implementations. 116 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 117 | pub struct MintReturn { 118 | /// The new balance of the owner address. 119 | pub balance: TokenAmount, 120 | /// The new total supply. 121 | pub supply: TokenAmount, 122 | /// (Optional) data returned from receiver hook. 123 | pub recipient_data: RawBytes, 124 | } 125 | 126 | /// Intermediate data used by mint_return to construct the return data. 127 | #[derive(Clone, Debug)] 128 | pub struct MintIntermediate { 129 | /// Recipient address to use for querying balance. 130 | pub recipient: Address, 131 | /// (Optional) data returned from receiver hook. 132 | pub recipient_data: RawBytes, 133 | } 134 | 135 | impl RecipientData for MintIntermediate { 136 | fn set_recipient_data(&mut self, data: RawBytes) { 137 | self.recipient_data = data; 138 | } 139 | } 140 | 141 | /// Instruction to transfer tokens to another address. 142 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 143 | pub struct TransferParams { 144 | pub to: Address, 145 | /// A non-negative amount to transfer. 146 | pub amount: TokenAmount, 147 | /// Arbitrary data to pass on via the receiver hook. 148 | pub operator_data: RawBytes, 149 | } 150 | 151 | /// Return value after a successful transfer. 152 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 153 | pub struct TransferReturn { 154 | /// The new balance of the `from` address. 155 | pub from_balance: TokenAmount, 156 | /// The new balance of the `to` address. 157 | pub to_balance: TokenAmount, 158 | /// (Optional) data returned from receiver hook. 159 | pub recipient_data: RawBytes, 160 | } 161 | 162 | /// Intermediate data used by transfer_return to construct the return data. 163 | #[derive(Debug)] 164 | pub struct TransferIntermediate { 165 | pub from: Address, 166 | pub to: Address, 167 | /// (Optional) data returned from receiver hook. 168 | pub recipient_data: RawBytes, 169 | } 170 | 171 | impl RecipientData for TransferIntermediate { 172 | fn set_recipient_data(&mut self, data: RawBytes) { 173 | self.recipient_data = data; 174 | } 175 | } 176 | 177 | /// Instruction to transfer tokens between two addresses as an operator. 178 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 179 | pub struct TransferFromParams { 180 | pub from: Address, 181 | pub to: Address, 182 | /// A non-negative amount to transfer. 183 | pub amount: TokenAmount, 184 | /// Arbitrary data to pass on via the receiver hook. 185 | pub operator_data: RawBytes, 186 | } 187 | 188 | /// Return value after a successful delegated transfer. 189 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 190 | pub struct TransferFromReturn { 191 | /// The new balance of the `from` address. 192 | pub from_balance: TokenAmount, 193 | /// The new balance of the `to` address. 194 | pub to_balance: TokenAmount, 195 | /// The new remaining allowance between `owner` and `operator` (caller). 196 | pub allowance: TokenAmount, 197 | /// (Optional) data returned from receiver hook. 198 | pub recipient_data: RawBytes, 199 | } 200 | 201 | /// Intermediate data used by transfer_from_return to construct the return data. 202 | #[derive(Clone, Debug)] 203 | pub struct TransferFromIntermediate { 204 | pub operator: Address, 205 | pub from: Address, 206 | pub to: Address, 207 | /// (Optional) data returned from receiver hook. 208 | pub recipient_data: RawBytes, 209 | } 210 | 211 | impl RecipientData for TransferFromIntermediate { 212 | fn set_recipient_data(&mut self, data: RawBytes) { 213 | self.recipient_data = data; 214 | } 215 | } 216 | 217 | /// Instruction to increase an allowance between two addresses. 218 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 219 | pub struct IncreaseAllowanceParams { 220 | pub operator: Address, 221 | /// A non-negative amount to increase the allowance by. 222 | pub increase: TokenAmount, 223 | } 224 | 225 | /// Instruction to decrease an allowance between two addresses. 226 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 227 | pub struct DecreaseAllowanceParams { 228 | pub operator: Address, 229 | /// A non-negative amount to decrease the allowance by. 230 | pub decrease: TokenAmount, 231 | } 232 | 233 | /// Instruction to revoke (set to 0) an allowance. 234 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 235 | pub struct RevokeAllowanceParams { 236 | pub operator: Address, 237 | } 238 | 239 | /// Params to get allowance between to addresses. 240 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 241 | pub struct GetAllowanceParams { 242 | pub owner: Address, 243 | pub operator: Address, 244 | } 245 | 246 | /// Instruction to burn an amount of tokens. 247 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 248 | pub struct BurnParams { 249 | /// A non-negative amount to burn. 250 | pub amount: TokenAmount, 251 | } 252 | 253 | /// The updated value after burning. 254 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 255 | pub struct BurnReturn { 256 | /// New balance in the account after the successful burn. 257 | pub balance: TokenAmount, 258 | } 259 | 260 | /// Instruction to burn an amount of tokens from another address. 261 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 262 | pub struct BurnFromParams { 263 | pub owner: Address, 264 | /// A non-negative amount to burn. 265 | pub amount: TokenAmount, 266 | } 267 | 268 | /// The updated value after a delegated burn. 269 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 270 | pub struct BurnFromReturn { 271 | /// New balance in the account after the successful burn. 272 | pub balance: TokenAmount, 273 | /// New remaining allowance between the owner and operator (caller). 274 | pub allowance: TokenAmount, 275 | } 276 | -------------------------------------------------------------------------------- /frc53_nft/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "frc53_nft" 4 | description = "Filecoin FRC-0053 non-fungible token reference implementation" 5 | version = "8.0.0" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["filecoin", "fvm", "token", "nft", "frc-0053"] 8 | repository = "https://github.com/filecoin-project/actors-utils" 9 | edition = "2021" 10 | 11 | [dependencies] 12 | frc42_dispatch = { workspace = true } 13 | fvm_actor_utils = { workspace = true } 14 | 15 | cid = { workspace = true } 16 | fvm_ipld_bitfield = { workspace = true } 17 | fvm_ipld_blockstore = { workspace = true } 18 | fvm_ipld_hamt = { workspace = true } 19 | fvm_ipld_amt = { workspace = true } 20 | fvm_ipld_encoding = { workspace = true } 21 | fvm_sdk = { workspace = true } 22 | fvm_shared = { workspace = true } 23 | integer-encoding = { workspace = true } 24 | multihash-codetable = { workspace = true, features = ["blake2b"] } 25 | num-traits = { workspace = true } 26 | serde = { workspace = true } 27 | thiserror = { workspace = true } 28 | -------------------------------------------------------------------------------- /frc53_nft/README.md: -------------------------------------------------------------------------------- 1 | # frc53_nft 2 | 3 | This acts as the reference library for FRC53. While remaining complaint with the 4 | spec, this library is opinionated in its batching, minting and storage 5 | strategies to optimize for common usage patterns. 6 | 7 | For example, write operations are generally optimised over read operations as 8 | on-chain state can be read by direct inspection (rather than via an actor call) 9 | in many cases. 10 | -------------------------------------------------------------------------------- /frc53_nft/src/receiver.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::method_hash; 2 | use fvm_actor_utils::receiver::{ReceiverHook, ReceiverHookError, ReceiverType, RecipientData}; 3 | use fvm_ipld_encoding::tuple::*; 4 | use fvm_ipld_encoding::RawBytes; 5 | use fvm_shared::{address::Address, ActorID}; 6 | 7 | use crate::types::TokenID; 8 | 9 | pub const FRC53_TOKEN_TYPE: ReceiverType = method_hash!("FRC53") as u32; 10 | 11 | pub trait FRC53ReceiverHook { 12 | fn new_frc53( 13 | address: Address, 14 | frc53_params: FRC53TokenReceived, 15 | result_data: T, 16 | ) -> std::result::Result, ReceiverHookError>; 17 | } 18 | 19 | impl FRC53ReceiverHook for ReceiverHook { 20 | /// Construct a new FRC46 [`ReceiverHook`] call. 21 | fn new_frc53( 22 | address: Address, 23 | frc53_params: FRC53TokenReceived, 24 | result_data: T, 25 | ) -> std::result::Result, ReceiverHookError> { 26 | Ok(ReceiverHook::new( 27 | address, 28 | RawBytes::serialize(frc53_params)?, 29 | FRC53_TOKEN_TYPE, 30 | result_data, 31 | )) 32 | } 33 | } 34 | 35 | /// Receive parameters for an FRC53 token. 36 | #[derive(Serialize_tuple, Deserialize_tuple, PartialEq, Eq, Clone, Debug)] 37 | pub struct FRC53TokenReceived { 38 | /// The account that the tokens are being sent to (the receiver address). 39 | pub to: ActorID, 40 | /// Address of the operator that initiated the transfer/mint. 41 | pub operator: ActorID, 42 | /// Amount of tokens being transferred/minted. 43 | pub token_ids: Vec, 44 | /// Data specified by the operator during transfer/mint. 45 | pub operator_data: RawBytes, 46 | /// Additional data specified by the token-actor during transfer/mint. 47 | pub token_data: RawBytes, 48 | } 49 | -------------------------------------------------------------------------------- /frc53_nft/src/types.rs: -------------------------------------------------------------------------------- 1 | //! Interfaces and types for the frc53 NFT standard 2 | use cid::Cid; 3 | use fvm_actor_utils::receiver::RecipientData; 4 | use fvm_ipld_bitfield::BitField; 5 | use fvm_ipld_encoding::tuple::*; 6 | use fvm_ipld_encoding::RawBytes; 7 | use fvm_shared::address::Address; 8 | use fvm_shared::ActorID; 9 | 10 | #[cfg(doc)] 11 | use super::state::Cursor; 12 | 13 | pub type TokenID = u64; 14 | 15 | /// Multiple token IDs are represented as a BitField encoded with RLE+ the index of each set bit 16 | /// corresponds to a TokenID. 17 | pub type TokenSet = BitField; 18 | 19 | /// Multiple actor IDs are represented as a BitField encoded with RLE+ the index of each set bit 20 | /// corresponds to a ActorID. 21 | pub type ActorIDSet = BitField; 22 | 23 | /// A trait to be implemented by FRC-0053 compliant actors. 24 | pub trait FRC53NFT { 25 | /// A descriptive name for the collection of NFTs in this actor. 26 | fn name(&self) -> String; 27 | 28 | /// An abbreviated name for NFTs in this contract. 29 | fn symbol(&self) -> String; 30 | 31 | /// Gets a link to associated metadata for a given NFT. 32 | fn metadata(&self, params: TokenID) -> Cid; 33 | 34 | /// Gets the total number of NFTs in this actor. 35 | fn total_supply(&self) -> u64; 36 | 37 | /// Burns a given NFT, removing it from the total supply and preventing new NFTs from being 38 | /// minted with the same ID. 39 | fn burn(&self, token_id: TokenID); 40 | 41 | /// Gets a list of all the tokens in the collection. 42 | // FIXME: make this paginated 43 | fn list_tokens(&self) -> Vec; 44 | 45 | /// Gets the number of tokens held by a particular address (if it exists). 46 | fn balance_of(&self, owner: Address) -> u64; 47 | 48 | /// Returns the owner of the NFT specified by `token_id`. 49 | fn owner_of(&self, token_id: TokenID) -> ActorID; 50 | 51 | /// Transfers specific NFTs from the caller to another account. 52 | fn transfer(&self, params: TransferParams); 53 | 54 | /// Transfers specific NFTs between the [`from`][`TransferFromParams::from`] and 55 | /// [`to`][`TransferFromParams::to`] addresses. 56 | fn transfer_from(&self, params: TransferFromParams); 57 | 58 | /// Change or reaffirm the approved address for a set of NFTs, setting to zero means there is no 59 | /// approved address. 60 | fn approve(&self, params: ApproveParams); 61 | 62 | /// Set approval for all, allowing an operator to control all of the caller's tokens (including 63 | /// future tokens) until approval is revoked. 64 | fn set_approval_for_all(&self, params: ApproveForAllParams); 65 | 66 | /// Get the approved address for a single NFT. 67 | fn get_approved(&self, params: TokenID) -> ActorID; 68 | 69 | /// Query if the address is the approved operator for another address. 70 | fn is_approved_for_all(&self, params: IsApprovedForAllParams) -> bool; 71 | } 72 | 73 | /// Return value after a successful mint. 74 | /// 75 | /// The mint method is not standardised, so this is merely a useful library-level type, and 76 | /// recommendation for token implementations. 77 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 78 | pub struct MintReturn { 79 | /// The new balance of the owner address. 80 | pub balance: u64, 81 | /// The new total supply. 82 | pub supply: u64, 83 | /// List of the tokens that were minted successfully (some may have been burned during hook 84 | /// execution). 85 | pub token_ids: Vec, 86 | /// (Optional) data returned from the receiver hook. 87 | pub recipient_data: RawBytes, 88 | } 89 | 90 | /// Intermediate data used by mint_return to construct the return data. 91 | #[derive(Clone, Debug)] 92 | pub struct MintIntermediate { 93 | /// Receiving address used for querying balance. 94 | pub to: ActorID, 95 | /// List of the newly minted tokens. 96 | pub token_ids: Vec, 97 | /// (Optional) data returned from the receiver hook. 98 | pub recipient_data: RawBytes, 99 | } 100 | 101 | impl RecipientData for MintIntermediate { 102 | fn set_recipient_data(&mut self, data: RawBytes) { 103 | self.recipient_data = data; 104 | } 105 | } 106 | 107 | /// Intermediate data used by [`NFT::transfer_return`][`super::NFT::transfer_return`] to construct 108 | /// the return data. 109 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 110 | pub struct TransferIntermediate { 111 | pub token_ids: Vec, 112 | pub from: ActorID, 113 | pub to: ActorID, 114 | /// (Optional) data returned from the receiver hook. 115 | pub recipient_data: RawBytes, 116 | } 117 | 118 | impl RecipientData for TransferIntermediate { 119 | fn set_recipient_data(&mut self, data: RawBytes) { 120 | self.recipient_data = data; 121 | } 122 | } 123 | 124 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 125 | pub struct TransferParams { 126 | pub to: Address, 127 | pub token_ids: Vec, 128 | pub operator_data: RawBytes, 129 | } 130 | 131 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 132 | pub struct TransferReturn { 133 | pub from_balance: u64, 134 | pub to_balance: u64, 135 | pub token_ids: Vec, 136 | } 137 | 138 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 139 | pub struct TransferFromParams { 140 | pub from: Address, 141 | pub to: Address, 142 | pub token_ids: Vec, 143 | pub operator_data: RawBytes, 144 | } 145 | 146 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 147 | pub struct BurnFromParams { 148 | pub from: Address, 149 | pub token_ids: Vec, 150 | } 151 | 152 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 153 | pub struct ApproveParams { 154 | pub operator: Address, 155 | pub token_ids: Vec, 156 | } 157 | 158 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 159 | pub struct ApproveForAllParams { 160 | pub operator: Address, 161 | } 162 | 163 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 164 | pub struct IsApprovedForAllParams { 165 | pub owner: Address, 166 | pub operator: Address, 167 | } 168 | 169 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 170 | pub struct RevokeParams { 171 | pub operator: Address, 172 | pub token_ids: Vec, 173 | } 174 | 175 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 176 | pub struct RevokeForAllParams { 177 | pub operator: Address, 178 | } 179 | 180 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 181 | pub struct ListTokensParams { 182 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 183 | pub cursor: RawBytes, 184 | pub limit: u64, 185 | } 186 | 187 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 188 | pub struct ListTokensReturn { 189 | pub tokens: BitField, 190 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 191 | pub next_cursor: Option, 192 | } 193 | 194 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 195 | pub struct ListOwnedTokensParams { 196 | pub owner: Address, 197 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 198 | pub cursor: RawBytes, 199 | pub limit: u64, 200 | } 201 | 202 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 203 | pub struct ListOwnedTokensReturn { 204 | pub tokens: TokenSet, 205 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 206 | pub next_cursor: Option, 207 | } 208 | 209 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 210 | pub struct ListTokenOperatorsParams { 211 | pub token_id: TokenID, 212 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 213 | pub cursor: RawBytes, 214 | pub limit: u64, 215 | } 216 | 217 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 218 | pub struct ListTokenOperatorsReturn { 219 | pub operators: ActorIDSet, 220 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 221 | pub next_cursor: Option, 222 | } 223 | 224 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 225 | pub struct ListOperatorTokensParams { 226 | pub operator: Address, 227 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 228 | pub cursor: RawBytes, 229 | pub limit: u64, 230 | } 231 | 232 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 233 | pub struct ListOperatorTokensReturn { 234 | pub tokens: TokenSet, 235 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 236 | pub next_cursor: Option, 237 | } 238 | 239 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 240 | pub struct ListAccountOperatorsParams { 241 | pub owner: Address, 242 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 243 | pub cursor: RawBytes, 244 | pub limit: u64, 245 | } 246 | 247 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 248 | pub struct ListAccountOperatorsReturn { 249 | pub operators: ActorIDSet, 250 | /// Opaque serialisation of [`Cursor`], with empty cursor meaning start of list. 251 | pub next_cursor: Option, 252 | } 253 | -------------------------------------------------------------------------------- /frc53_nft/src/util.rs: -------------------------------------------------------------------------------- 1 | use fvm_ipld_bitfield::BitField; 2 | use fvm_shared::ActorID; 3 | 4 | pub trait OperatorSet { 5 | /// Attempts to add the operator to the authorised list. 6 | /// 7 | /// Returns true if the operator was added, false if it was already present. 8 | fn add_operator(&mut self, operator: ActorID); 9 | 10 | /// Removes the operator from the authorised list. 11 | fn remove_operator(&mut self, operator: &ActorID); 12 | 13 | /// Checks if the operator is present in the list. 14 | fn contains_actor(&self, operator: &ActorID) -> bool; 15 | } 16 | 17 | impl OperatorSet for BitField { 18 | fn add_operator(&mut self, operator: ActorID) { 19 | self.set(operator); 20 | } 21 | 22 | fn remove_operator(&mut self, operator: &ActorID) { 23 | self.unset(*operator); 24 | } 25 | 26 | fn contains_actor(&self, operator: &ActorID) -> bool { 27 | self.get(*operator) 28 | } 29 | } 30 | 31 | // TODO: benchmark this against some other options such as 32 | // - BTreeSet in memory, Vec serialized 33 | // - BTreeSet in memory and serialization 34 | // - HashSets... 35 | // - Hamt 36 | // - Amt 37 | 38 | /// Maintains set-like invariants in-memory by maintaining sorted order of the underlying array. 39 | /// 40 | /// Insertion and deletion are O(n) operations but we expect operator lists to be a relatively small 41 | /// size. 42 | impl OperatorSet for Vec { 43 | fn add_operator(&mut self, id: ActorID) { 44 | if let Err(pos) = self.binary_search(&id) { 45 | self.insert(pos, id); 46 | } 47 | } 48 | 49 | fn remove_operator(&mut self, id: &ActorID) { 50 | if let Ok(pos) = self.binary_search(id) { 51 | self.remove(pos); 52 | } 53 | } 54 | 55 | fn contains_actor(&self, id: &ActorID) -> bool { 56 | self.binary_search(id).is_ok() 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod vec_test { 62 | use fvm_shared::ActorID; 63 | 64 | use super::OperatorSet; 65 | 66 | #[test] 67 | fn test_idempotent_add() { 68 | let mut operators: Vec = vec![]; 69 | operators.add_operator(1); 70 | operators.add_operator(1); 71 | operators.add_operator(1); 72 | 73 | assert!(operators.contains_actor(&1)); 74 | assert!(operators.len() == 1); 75 | 76 | operators.add_operator(2); 77 | operators.add_operator(2); 78 | operators.add_operator(1); 79 | assert!(operators.contains_actor(&1)); 80 | assert!(operators.contains_actor(&2)); 81 | assert!(operators.len() == 2); 82 | } 83 | 84 | #[test] 85 | fn test_ordered_add() { 86 | let mut operators: Vec = vec![]; 87 | operators.add_operator(2); 88 | operators.add_operator(3); 89 | operators.add_operator(5); 90 | operators.add_operator(1); 91 | operators.add_operator(4); 92 | 93 | assert!(operators.len() == 5); 94 | assert_eq!(operators, vec![1, 2, 3, 4, 5]); 95 | } 96 | 97 | #[test] 98 | fn test_removal() { 99 | let mut operators: Vec = vec![]; 100 | operators.add_operator(2); 101 | operators.add_operator(3); 102 | operators.add_operator(5); 103 | operators.add_operator(1); 104 | operators.add_operator(4); 105 | 106 | operators.remove_operator(&2); 107 | operators.remove_operator(&2); 108 | 109 | assert!(operators.len() == 4); 110 | assert_eq!(operators, vec![1, 3, 4, 5]); 111 | 112 | operators.remove_operator(&4); 113 | operators.remove_operator(&4); 114 | assert!(operators.len() == 3); 115 | assert_eq!(operators, vec![1, 3, 5]); 116 | } 117 | } 118 | 119 | #[cfg(test)] 120 | mod bitfield_test { 121 | use fvm_ipld_bitfield::BitField; 122 | 123 | use super::OperatorSet; 124 | 125 | #[test] 126 | fn test_idempotent_add() { 127 | let mut operators: BitField = BitField::default(); 128 | operators.add_operator(1); 129 | operators.add_operator(1); 130 | operators.add_operator(1); 131 | 132 | assert!(operators.contains_actor(&1)); 133 | assert!(operators.len() == 1); 134 | 135 | operators.add_operator(2); 136 | operators.add_operator(2); 137 | operators.add_operator(1); 138 | assert!(operators.contains_actor(&1)); 139 | assert!(operators.contains_actor(&2)); 140 | assert!(operators.len() == 2); 141 | } 142 | 143 | #[test] 144 | fn test_ordered_add() { 145 | let mut operators: BitField = BitField::default(); 146 | operators.add_operator(2); 147 | operators.add_operator(3); 148 | operators.add_operator(5); 149 | operators.add_operator(1); 150 | operators.add_operator(4); 151 | 152 | assert!(operators.len() == 5); 153 | assert!(operators.get(1)); 154 | assert!(operators.get(2)); 155 | assert!(operators.get(3)); 156 | assert!(operators.get(4)); 157 | assert!(operators.get(5)); 158 | } 159 | 160 | #[test] 161 | fn test_removal() { 162 | let mut operators: BitField = BitField::default(); 163 | operators.add_operator(2); 164 | operators.add_operator(3); 165 | operators.add_operator(5); 166 | operators.add_operator(1); 167 | operators.add_operator(4); 168 | 169 | operators.remove_operator(&2); 170 | operators.remove_operator(&2); 171 | 172 | assert!(operators.len() == 4); 173 | assert!(operators.get(1)); 174 | assert!(!operators.get(2)); 175 | assert!(operators.get(3)); 176 | assert!(operators.get(4)); 177 | assert!(operators.get(5)); 178 | 179 | operators.remove_operator(&4); 180 | 181 | assert!(operators.len() == 3); 182 | assert!(operators.get(1)); 183 | assert!(!operators.get(2)); 184 | assert!(operators.get(3)); 185 | assert!(!operators.get(4)); 186 | assert!(operators.get(5)); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /fvm_actor_utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fvm_actor_utils" 3 | description = "Utils for authoring native actors for the Filecoin Virtual Machine" 4 | version = "14.0.0" 5 | license = "MIT OR Apache-2.0" 6 | keywords = ["filecoin", "fvm"] 7 | repository = "https://github.com/filecoin-project/actors-utils" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | frc42_dispatch = { workspace = true } 12 | 13 | anyhow = { workspace = true } 14 | cid = { workspace = true } 15 | fvm_ipld_blockstore = { workspace = true } 16 | fvm_ipld_encoding = { workspace = true } 17 | fvm_shared = { workspace = true } 18 | fvm_sdk = { workspace = true } 19 | multihash-codetable = { workspace = true } 20 | num-traits = { workspace = true } 21 | serde = { workspace = true } 22 | thiserror = { workspace = true } 23 | -------------------------------------------------------------------------------- /fvm_actor_utils/README.md: -------------------------------------------------------------------------------- 1 | # fvm_actor_utils 2 | 3 | A set of abstractions to help work with the runtime layer when developing FVM 4 | native actors. This crate provides implementations backed by `fvm_sdk` which are 5 | suitable for use in Rust actors as well as mock implementations suitable for use 6 | in unit-tests. 7 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/actor.rs: -------------------------------------------------------------------------------- 1 | use cid::Cid; 2 | use fvm_sdk as sdk; 3 | use fvm_sdk::error::StateReadError; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Clone, Debug)] 7 | pub enum ActorError { 8 | #[error("root state not found {0}")] 9 | NoState(#[from] StateReadError), 10 | } 11 | 12 | type Result = std::result::Result; 13 | 14 | /// Generic utils related to actors on the FVM. 15 | pub trait Actor { 16 | /// Get the root cid of the actor's state. 17 | fn root_cid(&self) -> Result; 18 | } 19 | 20 | /// A helper handle for actors deployed on FVM. 21 | pub struct FvmActor {} 22 | 23 | impl Actor for FvmActor { 24 | fn root_cid(&self) -> Result { 25 | Ok(sdk::sself::root()?) 26 | } 27 | } 28 | 29 | /// A fake actor fixture that can be twiddled for testing. 30 | #[derive(Default, Clone, Debug)] 31 | pub struct FakeActor { 32 | pub root: Cid, 33 | } 34 | 35 | impl Actor for FakeActor { 36 | fn root_cid(&self) -> Result { 37 | Ok(self.root) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/blockstore.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use anyhow::Result; 3 | use cid::Cid; 4 | use fvm_ipld_blockstore::Block; 5 | use fvm_sdk::ipld; 6 | use multihash_codetable::Code; 7 | 8 | /// A blockstore that delegates to IPLD syscalls. 9 | #[derive(Default, Debug, Copy, Clone)] 10 | pub struct Blockstore; 11 | 12 | /// Blockstore implementation is borrowed from [the builtin actors][source]. This impl will likely 13 | /// be made redundant if low-level SDKs export blockstore implementations. 14 | /// 15 | /// [source]: https://github.com/filecoin-project/builtin-actors/blob/6df845dcdf9872beb6e871205eb34dcc8f7550b5/runtime/src/runtime/actor_blockstore.rs 16 | impl fvm_ipld_blockstore::Blockstore for Blockstore { 17 | fn get(&self, cid: &Cid) -> Result>> { 18 | // If this fails, the _CID_ is invalid. I.e., we have a bug. 19 | ipld::get(cid).map(Some).map_err(|e| anyhow!("get failed with {:?} on CID '{}'", e, cid)) 20 | } 21 | 22 | fn put_keyed(&self, k: &Cid, block: &[u8]) -> Result<()> { 23 | let code = Code::try_from(k.hash().code()).map_err(|e| anyhow!(e.to_string()))?; 24 | let k2 = self.put(code, &Block::new(k.codec(), block))?; 25 | if k != &k2 { 26 | return Err(anyhow!("put block with cid {} but has cid {}", k, k2)); 27 | } 28 | Ok(()) 29 | } 30 | 31 | fn put(&self, code: Code, block: &Block) -> Result 32 | where 33 | D: AsRef<[u8]>, 34 | { 35 | // TODO: Don't hard-code the size. Unfortunately, there's no good way to get it from the 36 | // codec at the moment. 37 | const SIZE: u32 = 32; 38 | let k = ipld::put(code.into(), SIZE, block.codec, block.data.as_ref()) 39 | .map_err(|e| anyhow!("put failed with {:?}", e))?; 40 | Ok(k) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod actor; 2 | pub mod blockstore; 3 | pub mod messaging; 4 | pub mod receiver; 5 | 6 | pub mod shared_blockstore; 7 | pub mod syscalls; 8 | pub mod util; 9 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/messaging.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::method_hash; 2 | use fvm_ipld_encoding::ipld_block::IpldBlock; 3 | use fvm_ipld_encoding::Error as IpldError; 4 | use fvm_sdk::{send, sys::ErrorNumber}; 5 | use fvm_shared::error::ExitCode; 6 | use fvm_shared::sys::SendFlags; 7 | use fvm_shared::{address::Address, econ::TokenAmount}; 8 | use fvm_shared::{MethodNum, Response}; 9 | use thiserror::Error; 10 | 11 | pub type Result = std::result::Result; 12 | 13 | #[derive(Error, Debug)] 14 | pub enum MessagingError { 15 | #[error("fvm syscall error: `{0}`")] 16 | Syscall(#[from] ErrorNumber), 17 | #[error("address could not be resolved: `{0}`")] 18 | AddressNotResolved(Address), 19 | #[error("address could not be initialized: `{0}`")] 20 | AddressNotInitialized(Address), 21 | #[error("ipld serialization error: `{0}`")] 22 | Ipld(#[from] IpldError), 23 | } 24 | 25 | impl From<&MessagingError> for ExitCode { 26 | fn from(error: &MessagingError) -> Self { 27 | match error { 28 | MessagingError::Syscall(e) => match e { 29 | ErrorNumber::IllegalArgument => ExitCode::USR_ILLEGAL_ARGUMENT, 30 | ErrorNumber::Forbidden | ErrorNumber::IllegalOperation => ExitCode::USR_FORBIDDEN, 31 | ErrorNumber::AssertionFailed => ExitCode::USR_ASSERTION_FAILED, 32 | ErrorNumber::InsufficientFunds => ExitCode::USR_INSUFFICIENT_FUNDS, 33 | ErrorNumber::IllegalCid | ErrorNumber::NotFound | ErrorNumber::InvalidHandle => { 34 | ExitCode::USR_NOT_FOUND 35 | } 36 | ErrorNumber::Serialization | ErrorNumber::IllegalCodec => { 37 | ExitCode::USR_SERIALIZATION 38 | } 39 | _ => ExitCode::USR_UNSPECIFIED, 40 | }, 41 | MessagingError::AddressNotResolved(_) | MessagingError::AddressNotInitialized(_) => { 42 | ExitCode::USR_NOT_FOUND 43 | } 44 | MessagingError::Ipld(_) => ExitCode::USR_SERIALIZATION, 45 | } 46 | } 47 | } 48 | 49 | /// An abstraction used to send messages to other actors. 50 | pub trait Messaging { 51 | /// Sends a message to an actor. 52 | fn send( 53 | &self, 54 | to: &Address, 55 | method: MethodNum, 56 | params: Option, 57 | value: TokenAmount, 58 | ) -> Result; 59 | } 60 | 61 | /// This method number comes from taking the name as "Receive" and applying 62 | /// the transformation described in [FRC-0042](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md). 63 | pub const RECEIVER_HOOK_METHOD_NUM: u64 = method_hash!("Receive"); 64 | 65 | #[derive(Debug, Default, Clone, Copy)] 66 | pub struct FvmMessenger {} 67 | 68 | impl Messaging for FvmMessenger { 69 | fn send( 70 | &self, 71 | to: &Address, 72 | method: MethodNum, 73 | params: Option, 74 | value: TokenAmount, 75 | ) -> Result { 76 | Ok(send::send(to, method, params, value, None, SendFlags::empty())?) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/receiver/mod.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use fvm_ipld_encoding::ipld_block::IpldBlock; 4 | use fvm_ipld_encoding::tuple::*; 5 | use fvm_ipld_encoding::RawBytes; 6 | use fvm_shared::{address::Address, econ::TokenAmount, error::ExitCode}; 7 | use num_traits::Zero; 8 | use thiserror::Error; 9 | 10 | use crate::messaging::{Messaging, MessagingError, RECEIVER_HOOK_METHOD_NUM}; 11 | 12 | /// Parameters for universal receiver. 13 | /// 14 | /// Actual payload varies with asset type. 15 | /// 16 | /// E.g.: `FRC46_TOKEN_TYPE` will come with a payload of `FRC46TokenReceived`. 17 | #[derive(Serialize_tuple, Deserialize_tuple, PartialEq, Eq, Clone, Debug)] 18 | pub struct UniversalReceiverParams { 19 | /// Asset type. 20 | pub type_: ReceiverType, 21 | /// Payload corresponding to asset type. 22 | pub payload: RawBytes, 23 | } 24 | 25 | /// Standard interface for an actor that wishes to receive FRC-0046 tokens or other assets. 26 | pub trait UniversalReceiver { 27 | /// Invoked by a token actor during pending transfer or mint to the receiver's address. 28 | /// 29 | /// Within this hook, the token actor has optimistically persisted the new balance so the 30 | /// receiving actor can immediately utilise the received funds. If the receiver wishes to reject 31 | /// the incoming transfer, this function should abort which will cause the token actor to 32 | /// rollback the transaction. 33 | fn receive(params: UniversalReceiverParams); 34 | } 35 | 36 | /// Type of asset received - could be tokens (FRC46 or other) or other assets. 37 | pub type ReceiverType = u32; 38 | 39 | #[derive(Error, Debug)] 40 | pub enum ReceiverHookError { 41 | #[error("receiver hook was not called")] 42 | NotCalled, 43 | #[error("receiver hook was already called")] 44 | AlreadyCalled, 45 | #[error("error encoding to ipld")] 46 | IpldEncoding(#[from] fvm_ipld_encoding::Error), 47 | #[error("error sending message")] 48 | Messaging(#[from] MessagingError), 49 | #[error("receiver hook error from {address:?}: exit_code={exit_code:?}, return_data={return_data:?}")] 50 | Receiver { address: Address, exit_code: ExitCode, return_data: RawBytes }, 51 | } 52 | 53 | impl ReceiverHookError { 54 | /// Construct a new [`ReceiverHookError::Receiver`]. 55 | pub fn new_receiver_error( 56 | address: Address, 57 | exit_code: ExitCode, 58 | return_data: Option, 59 | ) -> Self { 60 | Self::Receiver { 61 | address, 62 | exit_code, 63 | return_data: return_data.map_or(RawBytes::default(), |b| RawBytes::new(b.data)), 64 | } 65 | } 66 | } 67 | 68 | impl From<&ReceiverHookError> for ExitCode { 69 | fn from(error: &ReceiverHookError) -> Self { 70 | match error { 71 | ReceiverHookError::NotCalled | ReceiverHookError::AlreadyCalled => { 72 | ExitCode::USR_ASSERTION_FAILED 73 | } 74 | ReceiverHookError::IpldEncoding(_) => ExitCode::USR_SERIALIZATION, 75 | ReceiverHookError::Receiver { address: _, return_data: _, exit_code } => *exit_code, 76 | ReceiverHookError::Messaging(e) => e.into(), 77 | } 78 | } 79 | } 80 | 81 | pub trait RecipientData { 82 | fn set_recipient_data(&mut self, data: RawBytes); 83 | } 84 | 85 | /// Implements a guarded call to a token receiver hook. 86 | /// 87 | /// Mint and Transfer operations will return this so that state can be updated and saved before 88 | /// making the call into the receiver hook. 89 | /// 90 | /// This also tracks whether the call has been made or not, and will panic if dropped without 91 | /// calling the hook. 92 | #[derive(Debug)] 93 | pub struct ReceiverHook { 94 | address: Address, 95 | token_type: ReceiverType, 96 | token_params: RawBytes, 97 | called: bool, 98 | result_data: Option, 99 | } 100 | 101 | impl ReceiverHook { 102 | /// Construct a new ReceiverHook call. 103 | pub fn new( 104 | address: Address, 105 | token_params: RawBytes, 106 | token_type: ReceiverType, 107 | result_data: T, 108 | ) -> Self { 109 | ReceiverHook { 110 | address, 111 | token_params, 112 | token_type, 113 | called: false, 114 | result_data: Some(result_data), 115 | } 116 | } 117 | 118 | /// Call the receiver hook and return the result. 119 | /// 120 | /// Requires the same [`Messaging`] trait as the `Token`. E.g., `hook.call(token.msg())?;`. 121 | /// 122 | /// Returns: 123 | /// 124 | /// - An error if already called. 125 | /// - An error if the hook call aborted. 126 | /// - Any return data provided by the hook upon success. 127 | pub fn call(&mut self, msg: &dyn Messaging) -> std::result::Result { 128 | if self.called { 129 | return Err(ReceiverHookError::AlreadyCalled); 130 | } 131 | 132 | self.called = true; 133 | 134 | let params = UniversalReceiverParams { 135 | type_: self.token_type, 136 | payload: mem::take(&mut self.token_params), // once encoded and sent, we don't need this anymore 137 | }; 138 | 139 | let ret = msg.send( 140 | &self.address, 141 | RECEIVER_HOOK_METHOD_NUM, 142 | IpldBlock::serialize_cbor(¶ms).map_err(|e| { 143 | ReceiverHookError::IpldEncoding(fvm_ipld_encoding::Error { 144 | description: e.to_string(), 145 | protocol: fvm_ipld_encoding::CodecProtocol::Cbor, 146 | }) 147 | })?, 148 | TokenAmount::zero(), 149 | )?; 150 | 151 | match ret.exit_code { 152 | ExitCode::OK => { 153 | self.result_data.as_mut().unwrap().set_recipient_data( 154 | ret.return_data.map_or(RawBytes::default(), |b| RawBytes::new(b.data)), 155 | ); 156 | Ok(self.result_data.take().unwrap()) 157 | } 158 | abort_code => Err(ReceiverHookError::new_receiver_error( 159 | self.address, 160 | abort_code, 161 | ret.return_data, 162 | )), 163 | } 164 | } 165 | } 166 | 167 | /// Drop implements the panic if not called behaviour. 168 | impl std::ops::Drop for ReceiverHook { 169 | fn drop(&mut self) { 170 | if !self.called { 171 | panic!( 172 | "dropped before receiver hook was called on {:?} with {:?}", 173 | self.address, self.token_params 174 | ); 175 | } 176 | } 177 | } 178 | 179 | #[cfg(test)] 180 | mod test { 181 | use frc42_dispatch::method_hash; 182 | use fvm_ipld_blockstore::MemoryBlockstore; 183 | use fvm_ipld_encoding::RawBytes; 184 | use fvm_shared::address::Address; 185 | 186 | use super::{ReceiverHook, RecipientData}; 187 | use crate::{syscalls::fake_syscalls::FakeSyscalls, util::ActorRuntime}; 188 | 189 | const ALICE: Address = Address::new_id(2); 190 | 191 | struct TestReturn; 192 | 193 | impl RecipientData for TestReturn { 194 | fn set_recipient_data(&mut self, _data: RawBytes) {} 195 | } 196 | 197 | fn generate_hook() -> ReceiverHook { 198 | ReceiverHook::new( 199 | ALICE, 200 | RawBytes::default(), 201 | method_hash!("TestToken") as u32, 202 | TestReturn {}, 203 | ) 204 | } 205 | 206 | #[test] 207 | fn calls_hook() { 208 | let mut hook = generate_hook(); 209 | let util = ActorRuntime::::new_test_runtime(); 210 | assert!(util.syscalls.last_message.borrow().is_none()); 211 | hook.call(&util).unwrap(); 212 | assert!(util.syscalls.last_message.borrow().is_some()); 213 | } 214 | 215 | #[test] 216 | #[should_panic] 217 | fn panics_if_not_called() { 218 | let mut _hook = generate_hook(); 219 | // _hook should panic when dropped as we haven't called the hook 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/shared_blockstore.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use anyhow::Result; 4 | use cid::Cid; 5 | use fvm_ipld_blockstore::MemoryBlockstore; 6 | 7 | /// A shared wrapper around [`MemoryBlockstore`]. 8 | /// 9 | /// Clones of it will reference the same underlying [`MemoryBlockstore`], allowing for more complex 10 | /// unit testing. 11 | #[derive(Debug, Clone)] 12 | pub struct SharedMemoryBlockstore { 13 | store: Rc, 14 | } 15 | 16 | impl SharedMemoryBlockstore { 17 | pub fn new() -> Self { 18 | Self { store: Rc::new(MemoryBlockstore::new()) } 19 | } 20 | } 21 | 22 | impl Default for SharedMemoryBlockstore { 23 | fn default() -> Self { 24 | Self::new() 25 | } 26 | } 27 | 28 | // blockstore implementation, passes calls through to the underlying MemoryBlockstore 29 | impl fvm_ipld_blockstore::Blockstore for SharedMemoryBlockstore { 30 | /// Gets the block from the blockstore. 31 | fn get(&self, k: &Cid) -> Result>> { 32 | self.store.get(k) 33 | } 34 | 35 | /// Put a block with a pre-computed cid. 36 | /// 37 | /// If you don't yet know the CID, use put. Some blockstores will re-compute the CID internally 38 | /// even if you provide it. 39 | /// 40 | /// If you _do_ already know the CID, use this method as some blockstores _won't_ recompute it. 41 | fn put_keyed(&self, k: &Cid, block: &[u8]) -> Result<()> { 42 | self.store.put_keyed(k, block) 43 | } 44 | 45 | /// Checks if the blockstore has the specified block. 46 | fn has(&self, k: &Cid) -> Result { 47 | self.store.has(k) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/syscalls/fake_syscalls.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap}; 2 | 3 | use cid::Cid; 4 | use fvm_ipld_encoding::ipld_block::IpldBlock; 5 | use fvm_shared::{ 6 | address::Address, econ::TokenAmount, error::ErrorNumber, error::ExitCode, ActorID, Response, 7 | }; 8 | 9 | use super::Syscalls; 10 | 11 | #[derive(Clone, Default, Debug)] 12 | pub struct TestMessage { 13 | pub method: u64, 14 | pub params: Option, 15 | pub value: TokenAmount, 16 | } 17 | 18 | #[derive(Clone, Default, Debug)] 19 | pub struct FakeSyscalls { 20 | /// The root of the receiving actor. 21 | pub root: RefCell, 22 | /// The f0 ID of the receiving actor. 23 | pub actor_id: ActorID, 24 | 25 | /// Actor ID to return as caller ID. 26 | pub caller_id: RefCell, 27 | 28 | /// A map of addresses that were instantiated in this runtime. 29 | pub addresses: RefCell>, 30 | /// The next-to-allocate f0 address. 31 | pub next_actor_id: RefCell, 32 | 33 | /// The last message sent via this runtime. 34 | pub last_message: RefCell>, 35 | /// Flag to control message success. 36 | pub abort_next_send: RefCell, 37 | } 38 | 39 | impl FakeSyscalls { 40 | /// Set the ActorID returned as caller. 41 | pub fn set_caller_id(&self, new_id: ActorID) { 42 | self.caller_id.replace(new_id); 43 | } 44 | } 45 | 46 | impl Syscalls for FakeSyscalls { 47 | fn root(&self) -> Result { 48 | Ok(*self.root.borrow()) 49 | } 50 | 51 | fn set_root(&self, cid: &Cid) -> Result<(), super::NoStateError> { 52 | self.root.replace(*cid); 53 | Ok(()) 54 | } 55 | 56 | fn receiver(&self) -> fvm_shared::ActorID { 57 | self.actor_id 58 | } 59 | 60 | fn caller(&self) -> fvm_shared::ActorID { 61 | *self.caller_id.borrow() 62 | } 63 | 64 | fn send( 65 | &self, 66 | to: &fvm_shared::address::Address, 67 | method: fvm_shared::MethodNum, 68 | params: Option, 69 | value: fvm_shared::econ::TokenAmount, 70 | ) -> Result { 71 | if *self.abort_next_send.borrow() { 72 | self.abort_next_send.replace(false); 73 | Err(ErrorNumber::AssertionFailed) 74 | } else { 75 | // sending to an address instantiates it if it isn't already 76 | let mut map = self.addresses.borrow_mut(); 77 | 78 | match to.payload() { 79 | // TODO: in a real system, this is fallible if the address does not exist 80 | // This impl assumes that any f0 form address is in the map/instantiated but does not check so 81 | // Sending to actors should succeed if the actor exists but not instantiate it 82 | fvm_shared::address::Payload::ID(_) | fvm_shared::address::Payload::Actor(_) => { 83 | Ok(()) 84 | } 85 | // Sending to public keys should instantiate the actor 86 | fvm_shared::address::Payload::Secp256k1(_) 87 | | fvm_shared::address::Payload::BLS(_) 88 | | fvm_shared::address::Payload::Delegated(_) => { 89 | if !map.contains_key(to) { 90 | let actor_id = self.next_actor_id.replace_with(|old| *old + 1); 91 | map.insert(*to, actor_id); 92 | } 93 | Ok(()) 94 | } 95 | }?; 96 | 97 | // save the fake message as being sent 98 | let message = TestMessage { method, params: params.clone(), value }; 99 | self.last_message.replace(Some(message)); 100 | 101 | Ok(Response { exit_code: ExitCode::OK, return_data: params }) 102 | } 103 | } 104 | 105 | fn resolve_address(&self, addr: &Address) -> Option { 106 | // if it is already an ID-address, just return it 107 | if let fvm_shared::address::Payload::ID(id) = addr.payload() { 108 | return Some(*id); 109 | } 110 | 111 | let map = self.addresses.borrow(); 112 | map.get(addr).copied() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/syscalls/fvm_syscalls.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use fvm_ipld_blockstore::Blockstore; 3 | use fvm_ipld_encoding::ipld_block::IpldBlock; 4 | use fvm_sdk; 5 | use fvm_shared::sys::SendFlags; 6 | use fvm_shared::{address::Address, MethodNum, Response}; 7 | 8 | use super::Syscalls; 9 | use crate::util::ActorRuntime; 10 | 11 | /// Runtime that delegates to [`fvm_sdk`] allowing actors to be deployed on-chain. 12 | #[derive(Default, Debug, Clone, Copy)] 13 | pub struct FvmSyscalls {} 14 | 15 | impl Syscalls for FvmSyscalls { 16 | fn root(&self) -> Result { 17 | fvm_sdk::sself::root().map_err(|_| super::NoStateError) 18 | } 19 | 20 | fn set_root(&self, cid: &cid::Cid) -> Result<(), super::NoStateError> { 21 | fvm_sdk::sself::set_root(cid).map_err(|_| super::NoStateError) 22 | } 23 | 24 | fn receiver(&self) -> fvm_shared::ActorID { 25 | fvm_sdk::message::receiver() 26 | } 27 | 28 | fn caller(&self) -> fvm_shared::ActorID { 29 | fvm_sdk::message::caller() 30 | } 31 | 32 | fn send( 33 | &self, 34 | to: &Address, 35 | method: MethodNum, 36 | params: Option, 37 | value: fvm_shared::econ::TokenAmount, 38 | ) -> fvm_sdk::SyscallResult { 39 | match fvm_sdk::send::send(to, method, params, value, None, SendFlags::empty()) { 40 | Ok(res) => Ok(Response { exit_code: res.exit_code, return_data: res.return_data }), 41 | Err(err) => Err(err), 42 | } 43 | } 44 | 45 | fn resolve_address(&self, addr: &Address) -> Option { 46 | fvm_sdk::actor::resolve_address(addr) 47 | } 48 | } 49 | 50 | impl ActorRuntime { 51 | pub fn new_fvm_runtime() -> ActorRuntime { 52 | ActorRuntime { syscalls: FvmSyscalls::default(), blockstore: crate::blockstore::Blockstore } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/syscalls/mod.rs: -------------------------------------------------------------------------------- 1 | use cid::Cid; 2 | use fvm_ipld_encoding::ipld_block::IpldBlock; 3 | use fvm_shared::{ 4 | address::Address, econ::TokenAmount, error::ErrorNumber, ActorID, MethodNum, Response, 5 | }; 6 | use thiserror::Error; 7 | 8 | pub mod fake_syscalls; 9 | pub mod fvm_syscalls; 10 | 11 | /// Copied to avoid linking against `fvm_sdk` for non-WASM targets. 12 | #[derive(Copy, Clone, Debug, Error)] 13 | #[error("actor does not exist in state-tree")] 14 | pub struct NoStateError; 15 | 16 | /// The Syscalls trait defines methods available to the actor from its execution environment. 17 | /// 18 | /// The methods available are a subset of the methods exported by `fvm_sdk`. 19 | pub trait Syscalls { 20 | /// Get the IPLD root CID. Fails if the actor doesn't have state (before the first call to 21 | /// `set_root` and after actor deletion). 22 | fn root(&self) -> Result; 23 | 24 | /// Set the actor's state-tree root. 25 | /// 26 | /// Fails if: 27 | /// 28 | /// - The new root is not in the actor's "reachable" set. 29 | /// - Fails if the actor has been deleted. 30 | fn set_root(&self, cid: &Cid) -> Result<(), NoStateError>; 31 | 32 | /// Returns the ID address of the actor. 33 | fn receiver(&self) -> ActorID; 34 | 35 | /// Returns the ID address of the calling actor. 36 | fn caller(&self) -> ActorID; 37 | 38 | /// Sends a message to an actor. 39 | fn send( 40 | &self, 41 | to: &Address, 42 | method: MethodNum, 43 | params: Option, 44 | value: TokenAmount, 45 | ) -> Result; 46 | 47 | /// Resolves the ID address of an actor. 48 | /// 49 | /// Returns None if the address cannot be resolved. Successfully resolving an address doesn't 50 | /// necessarily mean the actor exists (e.g., if the address was already an actor ID). 51 | fn resolve_address(&self, addr: &Address) -> Option; 52 | } 53 | -------------------------------------------------------------------------------- /fvm_actor_utils/src/util.rs: -------------------------------------------------------------------------------- 1 | use cid::Cid; 2 | use fvm_ipld_blockstore::Blockstore; 3 | use fvm_ipld_blockstore::MemoryBlockstore; 4 | use fvm_ipld_encoding::ipld_block::IpldBlock; 5 | use fvm_shared::METHOD_SEND; 6 | use fvm_shared::{address::Address, econ::TokenAmount, error::ExitCode, ActorID}; 7 | use fvm_shared::{MethodNum, Response}; 8 | use num_traits::Zero; 9 | use thiserror::Error; 10 | 11 | use crate::messaging::{Messaging, MessagingError, Result as MessagingResult}; 12 | use crate::shared_blockstore::SharedMemoryBlockstore; 13 | use crate::syscalls::fake_syscalls::FakeSyscalls; 14 | use crate::syscalls::NoStateError; 15 | use crate::syscalls::Syscalls; 16 | 17 | #[derive(Error, Clone, Debug)] 18 | pub enum ActorError { 19 | #[error("root state not found {0}")] 20 | NoState(#[from] NoStateError), 21 | } 22 | 23 | type ActorResult = std::result::Result; 24 | 25 | impl From<&ActorError> for ExitCode { 26 | fn from(error: &ActorError) -> Self { 27 | match error { 28 | ActorError::NoState(_) => ExitCode::USR_NOT_FOUND, 29 | } 30 | } 31 | } 32 | 33 | /// [`ActorRuntime`] provides access to system resources via [`Syscalls`] and the [`Blockstore`]. 34 | /// 35 | /// It provides higher level utilities than raw syscalls for actors to use to interact with the 36 | /// IPLD layer and the FVM runtime (e.g. messaging other actors). 37 | #[derive(Clone, Debug)] 38 | pub struct ActorRuntime { 39 | pub syscalls: S, 40 | pub blockstore: BS, 41 | } 42 | 43 | impl ActorRuntime { 44 | pub fn new(syscalls: S, blockstore: BS) -> ActorRuntime { 45 | ActorRuntime { syscalls, blockstore } 46 | } 47 | 48 | /// Creates a runtime suitable for tests, using [mock syscalls][`FakeSyscalls`] and a 49 | /// [memory blockstore][`MemoryBlockstore`]. 50 | pub fn new_test_runtime() -> ActorRuntime { 51 | ActorRuntime { syscalls: FakeSyscalls::default(), blockstore: MemoryBlockstore::default() } 52 | } 53 | 54 | /// Creates a runtime suitable for more complex tests, using [mock syscalls][`FakeSyscalls`] and 55 | /// a [memory blockstore][`MemoryBlockstore`]. 56 | /// 57 | /// Clones of this runtime will reference the same blockstore. 58 | pub fn new_shared_test_runtime() -> ActorRuntime { 59 | ActorRuntime { 60 | syscalls: FakeSyscalls::default(), 61 | blockstore: SharedMemoryBlockstore::new(), 62 | } 63 | } 64 | 65 | /// Returns the address of the current actor as an [`ActorID`]. 66 | pub fn actor_id(&self) -> ActorID { 67 | self.syscalls.receiver() 68 | } 69 | 70 | pub fn caller(&self) -> ActorID { 71 | self.syscalls.caller() 72 | } 73 | 74 | /// Sends a message to an actor. 75 | pub fn send( 76 | &self, 77 | to: &Address, 78 | method: MethodNum, 79 | params: Option, 80 | value: TokenAmount, 81 | ) -> MessagingResult { 82 | Ok(self.syscalls.send(to, method, params, value)?) 83 | } 84 | 85 | /// Attempts to resolve the given address to its ID address form. 86 | /// 87 | /// Returns [`MessagingError::AddressNotResolved`] if the address could not be resolved. 88 | pub fn resolve_id(&self, address: &Address) -> MessagingResult { 89 | self.syscalls.resolve_address(address).ok_or(MessagingError::AddressNotResolved(*address)) 90 | } 91 | 92 | /// Resolves an address to an ID address, sending a message to initialize an account there if 93 | /// it doesn't exist. 94 | /// 95 | /// If the account cannot be created, this function returns 96 | /// [`MessagingError::AddressNotInitialized`]. 97 | pub fn resolve_or_init(&self, address: &Address) -> MessagingResult { 98 | let id = match self.resolve_id(address) { 99 | Ok(addr) => addr, 100 | Err(MessagingError::AddressNotResolved(_e)) => self.initialize_account(address)?, 101 | Err(e) => return Err(e), 102 | }; 103 | Ok(id) 104 | } 105 | 106 | pub fn initialize_account(&self, address: &Address) -> MessagingResult { 107 | self.send(address, METHOD_SEND, Default::default(), TokenAmount::zero())?; 108 | match self.resolve_id(address) { 109 | Ok(id) => Ok(id), 110 | Err(MessagingError::AddressNotResolved(e)) => { 111 | // if we can't resolve after the send, then the account was not initialized 112 | Err(MessagingError::AddressNotInitialized(e)) 113 | } 114 | Err(e) => Err(e), 115 | } 116 | } 117 | 118 | /// Get the root cid of the actor's state. 119 | pub fn root_cid(&self) -> ActorResult { 120 | Ok(self.syscalls.root().map_err(|_err| NoStateError)?) 121 | } 122 | 123 | /// Set the root cid of the actor's state. 124 | pub fn set_root(&self, cid: &Cid) -> ActorResult<()> { 125 | Ok(self.syscalls.set_root(cid).map_err(|_err| NoStateError)?) 126 | } 127 | 128 | /// Attempts to compare two addresses, seeing if they would resolve to the same Actor without 129 | /// actually instantiating accounts for them. 130 | /// 131 | /// If a and b are of the same type, simply do an equality check. Otherwise, attempt to resolve 132 | /// to an ActorID and compare. 133 | pub fn same_address(&self, address_a: &Address, address_b: &Address) -> bool { 134 | let protocol_a = address_a.protocol(); 135 | let protocol_b = address_b.protocol(); 136 | if protocol_a == protocol_b { 137 | address_a == address_b 138 | } else { 139 | // attempt to resolve both to ActorID 140 | let id_a = match self.resolve_id(address_a) { 141 | Ok(id) => id, 142 | Err(_) => return false, 143 | }; 144 | let id_b = match self.resolve_id(address_b) { 145 | Ok(id) => id, 146 | Err(_) => return false, 147 | }; 148 | id_a == id_b 149 | } 150 | } 151 | 152 | pub fn bs(&self) -> &BS { 153 | &self.blockstore 154 | } 155 | } 156 | 157 | /// Convenience impl encapsulating the blockstore functionality. 158 | impl Blockstore for ActorRuntime { 159 | fn get(&self, k: &Cid) -> anyhow::Result>> { 160 | self.blockstore.get(k) 161 | } 162 | 163 | fn put_keyed(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> { 164 | self.blockstore.put_keyed(k, block) 165 | } 166 | } 167 | 168 | impl Messaging for ActorRuntime { 169 | fn send( 170 | &self, 171 | to: &Address, 172 | method: fvm_shared::MethodNum, 173 | params: Option, 174 | value: fvm_shared::econ::TokenAmount, 175 | ) -> crate::messaging::Result { 176 | let res = self.syscalls.send(to, method, params, value); 177 | Ok(res?) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /fvm_dispatch_tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fvm_dispatch_tools" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | blake2b_simd = { workspace = true } 8 | clap = { workspace = true } 9 | frc42_dispatch = { workspace = true } 10 | -------------------------------------------------------------------------------- /fvm_dispatch_tools/src/blake2b.rs: -------------------------------------------------------------------------------- 1 | use blake2b_simd::blake2b; 2 | use frc42_dispatch::hash::Hasher; 3 | 4 | pub struct Blake2bHasher {} 5 | impl Hasher for Blake2bHasher { 6 | fn hash(&self, bytes: &[u8]) -> Vec { 7 | blake2b(bytes).as_bytes().to_vec() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fvm_dispatch_tools/src/main.rs: -------------------------------------------------------------------------------- 1 | mod blake2b; 2 | 3 | use std::io::{self, BufRead}; 4 | use std::process::exit; 5 | 6 | use blake2b::Blake2bHasher; 7 | use clap::Parser; 8 | use frc42_dispatch::hash::MethodResolver; 9 | 10 | const LONG_ABOUT: &str = 11 | "Pass a single method name as a command line argument or a list of method names, separated by \ 12 | new-lines to stdin. The output is a list of hashes, one per method name."; 13 | 14 | /// Takes a method name and converts it to an FRC-0042 compliant method number. 15 | /// 16 | /// Can be used by actor authors to precompute the method number for a given exported method to 17 | /// avoid runtime hasing during dispatch. 18 | #[derive(Parser, Debug)] 19 | #[clap( 20 | version, 21 | about, 22 | long_about = Some(LONG_ABOUT) 23 | )] 24 | struct Args { 25 | /// Method name to hash. 26 | method_name: Option, 27 | } 28 | 29 | fn main() { 30 | let args = Args::parse(); 31 | let resolver = MethodResolver::new(Blake2bHasher {}); 32 | let method_name = args.method_name; 33 | 34 | if method_name.is_none() { 35 | // read from std-in if no name passed in 36 | let stdin = io::stdin(); 37 | let mut handle = stdin.lock(); 38 | let mut line = String::new(); 39 | 40 | loop { 41 | let num_read = handle.read_line(&mut line).unwrap(); 42 | if num_read == 0 { 43 | break; 44 | } 45 | let method_name = line.trim().to_string(); 46 | let method_number = resolver.method_number(&method_name).unwrap(); 47 | println!("{method_number}"); 48 | line.clear(); 49 | } 50 | 51 | exit(0); 52 | } 53 | 54 | match resolver.method_number(method_name.unwrap().as_str()) { 55 | Ok(method_number) => { 56 | println!("{method_number}"); 57 | } 58 | Err(e) => { 59 | println!("Error computing method name: {e:?}") 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["clippy", "rustfmt"] 4 | targets = ["wasm32-unknown-unknown"] -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" -------------------------------------------------------------------------------- /testing/integration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helix_integration_tests" 3 | version = "0.1.0" 4 | repository = "https://github.com/helix-collective/filecoin" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | frc42_dispatch = { workspace = true } 10 | frc46_token = { workspace = true } 11 | frc53_nft = { workspace = true } 12 | fvm_actor_utils = { workspace = true } 13 | 14 | anyhow = { workspace = true, features = ["backtrace"] } 15 | cid = { workspace = true } 16 | fvm = { workspace = true } 17 | fvm_integration_tests = { workspace = true } 18 | fvm_ipld_blockstore = { workspace = true } 19 | fvm_ipld_bitfield = { workspace = true } 20 | fvm_ipld_encoding = { workspace = true } 21 | fvm_shared = { workspace = true } 22 | serde = { workspace = true } 23 | 24 | [dev-dependencies] 25 | helix_test_actors = { path = "../test_actors" } 26 | token_impl = { path = "../test_actors/actors/frc46_factory_token/token_impl" } 27 | -------------------------------------------------------------------------------- /testing/integration/tests/common/frc46_token_helpers.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::method_hash; 2 | use fvm::{executor::ApplyRet, externs::Externs}; 3 | use fvm_integration_tests::tester::Tester; 4 | use fvm_ipld_blockstore::Blockstore; 5 | use fvm_ipld_encoding::tuple::*; 6 | use fvm_ipld_encoding::RawBytes; 7 | use fvm_shared::{address::Address, bigint::Zero, econ::TokenAmount}; 8 | 9 | use super::TestHelpers; 10 | 11 | // this is here so we don't need to link every test against the basic_token_actor 12 | // otherwise we can't link against frc46_test_actor or any other test/example actors, 13 | // because the invoke() functions will conflict at link time 14 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 15 | pub struct MintParams { 16 | pub initial_owner: Address, 17 | pub amount: TokenAmount, 18 | pub operator_data: RawBytes, 19 | } 20 | 21 | /// Helper routines to simplify common token operations. 22 | pub trait TokenHelper { 23 | /// Get balance from token actor for a given address. 24 | /// 25 | /// This is a very common thing to check during tests. 26 | fn token_balance( 27 | &mut self, 28 | operator: Address, 29 | token_actor: Address, 30 | target: Address, 31 | ) -> TokenAmount; 32 | 33 | /// Mint tokens from token_actor to target address. 34 | fn mint_tokens( 35 | &mut self, 36 | operator: Address, 37 | token_actor: Address, 38 | target: Address, 39 | amount: TokenAmount, 40 | operator_data: RawBytes, 41 | ) -> ApplyRet; 42 | 43 | /// Mint tokens from token_actor to target address and assert a successful result. 44 | fn mint_tokens_ok( 45 | &mut self, 46 | operator: Address, 47 | token_actor: Address, 48 | target: Address, 49 | amount: TokenAmount, 50 | operator_data: RawBytes, 51 | ) -> ApplyRet; 52 | 53 | /// Check token balance, asserting that balance matches the provided amount. 54 | fn assert_token_balance( 55 | &mut self, 56 | operator: Address, 57 | token_actor: Address, 58 | target: Address, 59 | amount: TokenAmount, 60 | ); 61 | 62 | /// Check token balance, asserting a zero balance. 63 | fn assert_token_balance_zero( 64 | &mut self, 65 | operator: Address, 66 | token_actor: Address, 67 | target: Address, 68 | ); 69 | 70 | /// Get total supply of tokens. 71 | fn total_supply(&mut self, operator: Address, token_actor: Address) -> TokenAmount; 72 | 73 | /// Check total supply, asserting that it matches the given amount. 74 | fn assert_total_supply( 75 | &mut self, 76 | operator: Address, 77 | token_actor: Address, 78 | total_supply: TokenAmount, 79 | ); 80 | } 81 | 82 | impl TokenHelper for Tester { 83 | fn token_balance( 84 | &mut self, 85 | operator: Address, 86 | token_actor: Address, 87 | target: Address, 88 | ) -> TokenAmount { 89 | let params = RawBytes::serialize(target).unwrap(); 90 | let ret_val = 91 | self.call_method(operator, token_actor, method_hash!("BalanceOf"), Some(params)); 92 | ret_val.msg_receipt.return_data.deserialize::().unwrap() 93 | } 94 | 95 | fn mint_tokens( 96 | &mut self, 97 | operator: Address, 98 | token_actor: Address, 99 | target: Address, 100 | amount: TokenAmount, 101 | operator_data: RawBytes, 102 | ) -> ApplyRet { 103 | let mint_params = MintParams { initial_owner: target, amount, operator_data }; 104 | let params = RawBytes::serialize(mint_params).unwrap(); 105 | self.call_method(operator, token_actor, method_hash!("Mint"), Some(params)) 106 | } 107 | 108 | fn mint_tokens_ok( 109 | &mut self, 110 | operator: Address, 111 | token_actor: Address, 112 | target: Address, 113 | amount: TokenAmount, 114 | operator_data: RawBytes, 115 | ) -> ApplyRet { 116 | let ret = self.mint_tokens(operator, token_actor, target, amount, operator_data); 117 | assert!(ret.msg_receipt.exit_code.is_success(), "{ret:?}"); 118 | ret 119 | } 120 | 121 | fn assert_token_balance( 122 | &mut self, 123 | operator: Address, 124 | token_actor: Address, 125 | target: Address, 126 | amount: TokenAmount, 127 | ) { 128 | let balance = self.token_balance(operator, token_actor, target); 129 | assert_eq!(balance, amount); 130 | } 131 | 132 | fn assert_token_balance_zero( 133 | &mut self, 134 | operator: Address, 135 | token_actor: Address, 136 | target: Address, 137 | ) { 138 | let balance = self.token_balance(operator, token_actor, target); 139 | assert_eq!(balance, TokenAmount::zero()); 140 | } 141 | 142 | fn total_supply(&mut self, operator: Address, token_actor: Address) -> TokenAmount { 143 | let ret_val = self.call_method(operator, token_actor, method_hash!("TotalSupply"), None); 144 | ret_val.msg_receipt.return_data.deserialize::().unwrap() 145 | } 146 | 147 | fn assert_total_supply( 148 | &mut self, 149 | operator: Address, 150 | token_actor: Address, 151 | total_supply: TokenAmount, 152 | ) { 153 | let supply = self.total_supply(operator, token_actor); 154 | assert_eq!(supply, total_supply); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /testing/integration/tests/common/frc53_nft_helpers.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::method_hash; 2 | use frc53_nft::types::TokenID; 3 | use fvm::{executor::ApplyRet, externs::Externs}; 4 | use fvm_integration_tests::tester::Tester; 5 | use fvm_ipld_blockstore::Blockstore; 6 | use fvm_ipld_encoding::tuple::*; 7 | use fvm_ipld_encoding::RawBytes; 8 | use fvm_shared::{address::Address, ActorID}; 9 | 10 | use super::TestHelpers; 11 | 12 | // this is here so we don't need to link every test against the basic_token_actor 13 | // otherwise we can't link against frc46_test_actor or any other test/example actors, 14 | // because the invoke() functions will conflict at link time 15 | #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] 16 | pub struct MintParams { 17 | pub initial_owner: Address, 18 | pub metadata: Vec, 19 | pub operator_data: RawBytes, 20 | } 21 | 22 | pub trait NFTHelper { 23 | /// Get balance from token actor for a given address. 24 | /// 25 | /// This is a very common thing to check during tests. 26 | fn nft_balance(&mut self, operator: Address, token_actor: Address, target: Address) -> u64; 27 | 28 | /// Mint tokens from token_actor to target address. 29 | fn mint_nfts( 30 | &mut self, 31 | operator: Address, 32 | token_actor: Address, 33 | target: Address, 34 | amount: usize, 35 | operator_data: RawBytes, 36 | ) -> ApplyRet; 37 | 38 | /// Mint tokens from token_actor to target address and assert a successful result. 39 | fn mint_nfts_ok( 40 | &mut self, 41 | operator: Address, 42 | token_actor: Address, 43 | target: Address, 44 | amount: usize, 45 | operator_data: RawBytes, 46 | ) -> ApplyRet; 47 | 48 | /// Check token balance, asserting that balance matches the provided amount. 49 | fn assert_nft_balance( 50 | &mut self, 51 | operator: Address, 52 | token_actor: Address, 53 | target: Address, 54 | amount: u64, 55 | ); 56 | 57 | /// Check token balance, asserting a zero balance. 58 | fn assert_nft_balance_zero(&mut self, operator: Address, token_actor: Address, target: Address); 59 | 60 | /// Check the total supply, asserting that it matches the provided amount. 61 | fn assert_nft_total_supply(&mut self, operator: Address, token_actor: Address, amount: u64); 62 | 63 | /// Check the total supply, asserting that it is zero. 64 | fn assert_nft_total_supply_zero(&mut self, operator: Address, token_actor: Address); 65 | 66 | /// Check the tokens owner, asserting that it is owned by the specified ActorID. 67 | fn assert_nft_owner( 68 | &mut self, 69 | operator: Address, 70 | token_actor: Address, 71 | token_id: TokenID, 72 | owner: ActorID, 73 | ); 74 | 75 | /// Check the tokens metadata, asserting that it matches the provided metadata. 76 | fn assert_nft_metadata( 77 | &mut self, 78 | operator: Address, 79 | token_actor: Address, 80 | token_id: TokenID, 81 | metadata: String, 82 | ); 83 | } 84 | 85 | impl NFTHelper for Tester { 86 | fn nft_balance(&mut self, operator: Address, token_actor: Address, target: Address) -> u64 { 87 | let params = RawBytes::serialize(target).unwrap(); 88 | let ret_val = 89 | self.call_method(operator, token_actor, method_hash!("BalanceOf"), Some(params)); 90 | ret_val.msg_receipt.return_data.deserialize::().unwrap() 91 | } 92 | 93 | fn mint_nfts( 94 | &mut self, 95 | operator: Address, 96 | token_actor: Address, 97 | target: Address, 98 | amount: usize, 99 | operator_data: RawBytes, 100 | ) -> ApplyRet { 101 | let mint_params = MintParams { 102 | initial_owner: target, 103 | metadata: vec![String::default(); amount], 104 | operator_data, 105 | }; 106 | let params = RawBytes::serialize(mint_params).unwrap(); 107 | self.call_method(operator, token_actor, method_hash!("Mint"), Some(params)) 108 | } 109 | 110 | fn mint_nfts_ok( 111 | &mut self, 112 | operator: Address, 113 | token_actor: Address, 114 | target: Address, 115 | amount: usize, 116 | operator_data: RawBytes, 117 | ) -> ApplyRet { 118 | let ret = self.mint_nfts(operator, token_actor, target, amount, operator_data); 119 | assert!(ret.msg_receipt.exit_code.is_success()); 120 | ret 121 | } 122 | 123 | fn assert_nft_balance( 124 | &mut self, 125 | operator: Address, 126 | token_actor: Address, 127 | target: Address, 128 | amount: u64, 129 | ) { 130 | let balance = self.nft_balance(operator, token_actor, target); 131 | assert_eq!(balance, amount); 132 | } 133 | 134 | fn assert_nft_balance_zero( 135 | &mut self, 136 | operator: Address, 137 | token_actor: Address, 138 | target: Address, 139 | ) { 140 | let balance = self.nft_balance(operator, token_actor, target); 141 | assert_eq!(balance, 0); 142 | } 143 | 144 | fn assert_nft_total_supply(&mut self, operator: Address, token_actor: Address, amount: u64) { 145 | let ret_val = self.call_method(operator, token_actor, method_hash!("TotalSupply"), None); 146 | let total_supply = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 147 | assert_eq!(total_supply, amount); 148 | } 149 | 150 | fn assert_nft_total_supply_zero(&mut self, operator: Address, token_actor: Address) { 151 | self.assert_nft_total_supply(operator, token_actor, 0); 152 | } 153 | 154 | fn assert_nft_owner( 155 | &mut self, 156 | operator: Address, 157 | token_actor: Address, 158 | token_id: TokenID, 159 | actor: ActorID, 160 | ) { 161 | let params = RawBytes::serialize(token_id).unwrap(); 162 | let ret_val = 163 | self.call_method(operator, token_actor, method_hash!("OwnerOf"), Some(params)); 164 | let owner = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 165 | assert_eq!(owner, actor); 166 | } 167 | 168 | fn assert_nft_metadata( 169 | &mut self, 170 | operator: Address, 171 | token_actor: Address, 172 | token_id: TokenID, 173 | metadata: String, 174 | ) { 175 | let params = RawBytes::serialize(token_id).unwrap(); 176 | let ret_val = 177 | self.call_method(operator, token_actor, method_hash!("Metadata"), Some(params)); 178 | let owner = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 179 | assert_eq!(owner, metadata); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /testing/integration/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use std::env; 3 | 4 | use cid::Cid; 5 | use fvm::{ 6 | executor::{ApplyKind, ApplyRet, Executor}, 7 | externs::Externs, 8 | }; 9 | use fvm_integration_tests::{bundle, tester::Tester}; 10 | use fvm_ipld_blockstore::Blockstore; 11 | use fvm_ipld_encoding::RawBytes; 12 | use fvm_shared::{ 13 | address::Address, bigint::Zero, econ::TokenAmount, message::Message, state::StateTreeVersion, 14 | version::NetworkVersion, 15 | }; 16 | use serde::Serialize; 17 | 18 | pub mod frc46_token_helpers; 19 | pub mod frc53_nft_helpers; 20 | 21 | static BUNDLE_CAR: &[u8] = include_bytes!("../../../bundles/builtin-actors.car"); 22 | 23 | /// Helper routines to simplify common operations with a [`Tester`]. 24 | pub trait TestHelpers { 25 | /// Call a method on an actor. 26 | fn call_method( 27 | &mut self, 28 | from: Address, 29 | to: Address, 30 | method_num: u64, 31 | params: Option, 32 | ) -> ApplyRet; 33 | 34 | /// Call a method on an actor and assert a successful result. 35 | fn call_method_ok( 36 | &mut self, 37 | from: Address, 38 | to: Address, 39 | method_num: u64, 40 | params: Option, 41 | ) -> ApplyRet; 42 | 43 | /// Install an actor with initial state and ID. 44 | /// 45 | /// Returns the actor's address. 46 | fn install_actor_with_state( 47 | &mut self, 48 | code: &[u8], 49 | actor_id: u64, 50 | state: S, 51 | ) -> Address; 52 | 53 | /// Install an actor with no initial state. 54 | /// 55 | /// Takes ID and returns the new actor's [`Address`]. 56 | fn install_actor_stateless(&mut self, code: &[u8], actor_id: u64) -> Address; 57 | } 58 | 59 | #[allow(dead_code)] 60 | pub fn load_actor_wasm(path: &str) -> Vec { 61 | let wasm_path = env::current_dir().unwrap().join(path).canonicalize().unwrap(); 62 | std::fs::read(wasm_path).expect("unable to read actor file") 63 | } 64 | 65 | /// Construct a [`Tester`] with the provided [`Blockstore`]. 66 | /// 67 | /// This mainly cuts down on noise with importing the built-in actor bundle and network/state tree 68 | /// versions. 69 | pub fn construct_tester(blockstore: &BS) -> Tester { 70 | let bundle_root = bundle::import_bundle(&blockstore, BUNDLE_CAR).unwrap(); 71 | 72 | Tester::new(NetworkVersion::V21, StateTreeVersion::V5, bundle_root, blockstore.clone()).unwrap() 73 | } 74 | 75 | impl TestHelpers for Tester { 76 | fn call_method( 77 | &mut self, 78 | from: Address, 79 | to: Address, 80 | method_num: u64, 81 | params: Option, 82 | ) -> ApplyRet { 83 | static mut SEQUENCE: u64 = 0u64; 84 | let message = Message { 85 | from, 86 | to, 87 | gas_limit: 10_000_000_000, 88 | method_num, 89 | sequence: unsafe { SEQUENCE }, 90 | params: params.unwrap_or_default(), 91 | ..Message::default() 92 | }; 93 | unsafe { 94 | SEQUENCE += 1; 95 | } 96 | self.executor.as_mut().unwrap().execute_message(message, ApplyKind::Explicit, 100).unwrap() 97 | } 98 | 99 | fn call_method_ok( 100 | &mut self, 101 | from: Address, 102 | to: Address, 103 | method_num: u64, 104 | params: Option, 105 | ) -> ApplyRet { 106 | let ret = self.call_method(from, to, method_num, params); 107 | assert!(ret.msg_receipt.exit_code.is_success(), "call failed: {ret:#?}"); 108 | ret 109 | } 110 | 111 | fn install_actor_with_state( 112 | &mut self, 113 | code: &[u8], 114 | actor_id: u64, 115 | state: S, 116 | ) -> Address { 117 | let address = Address::new_id(actor_id); 118 | let state_cid = self.set_state(&state).unwrap(); 119 | self.set_actor_from_bin(code, state_cid, address, TokenAmount::zero()).unwrap(); 120 | address 121 | } 122 | 123 | fn install_actor_stateless(&mut self, code: &[u8], actor_id: u64) -> Address { 124 | let address = Address::new_id(actor_id); 125 | self.set_actor_from_bin(code, Cid::default(), address, TokenAmount::zero()).unwrap(); 126 | address 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /testing/integration/tests/frc46_single_actor_tests.rs: -------------------------------------------------------------------------------- 1 | use cid::Cid; 2 | use frc42_dispatch::method_hash; 3 | use frc46_token::token::types::MintReturn; 4 | use fvm_integration_tests::{dummy::DummyExterns, tester::Account}; 5 | use fvm_ipld_blockstore::MemoryBlockstore; 6 | use fvm_ipld_encoding::RawBytes; 7 | use fvm_shared::address::Address; 8 | use fvm_shared::bigint::Zero; 9 | use fvm_shared::{econ::TokenAmount, receipt::Receipt}; 10 | 11 | mod common; 12 | use common::frc46_token_helpers::TokenHelper; 13 | use common::{construct_tester, TestHelpers}; 14 | use fvm_ipld_encoding::tuple::*; 15 | use helix_test_actors::{FRC46_FACTORY_TOKEN_ACTOR_BINARY, FRC46_TEST_ACTOR_BINARY}; 16 | use serde::{Deserialize, Serialize}; 17 | use token_impl::ConstructorParams; 18 | 19 | /// This covers several simpler tests, which all involve a single receiving actor. 20 | /// 21 | /// They're combined because these integration tests take a long time to build and run. 22 | /// 23 | /// Test cases covered: 24 | /// 25 | /// - mint to test actor who rejects in receiver hook 26 | /// - mint to self (token actor - should be rejected) 27 | /// - mint to test actor who burns tokens upon receipt (calling Burn from within the hook) 28 | /// - test actor transfers back to token actor (should be rejected) 29 | /// - test actor transfers to self (zero amount) 30 | /// - test actor transfers to self (non-zero amount) 31 | /// - test actor transfers to self and rejects 32 | #[test] 33 | fn frc46_single_actor_tests() { 34 | let blockstore = MemoryBlockstore::default(); 35 | let mut tester = construct_tester(&blockstore); 36 | 37 | let operator: [Account; 1] = tester.create_accounts().unwrap(); 38 | 39 | let token_actor = tester.install_actor_stateless(FRC46_FACTORY_TOKEN_ACTOR_BINARY, 10000); 40 | let frc46_test_actor = Address::new_id(10001); 41 | tester 42 | .set_actor_from_bin( 43 | FRC46_TEST_ACTOR_BINARY, 44 | Cid::default(), 45 | frc46_test_actor, 46 | TokenAmount::zero(), 47 | ) 48 | .unwrap(); 49 | 50 | // Instantiate machine 51 | tester.instantiate_machine(DummyExterns).unwrap(); 52 | 53 | // construct our TEST token 54 | { 55 | let params = ConstructorParams { 56 | name: "Test Token".into(), 57 | symbol: "TEST".into(), 58 | granularity: 1, 59 | minter: operator[0].1, 60 | }; 61 | let params = RawBytes::serialize(params).unwrap(); 62 | let ret_val = tester.call_method( 63 | operator[0].1, 64 | token_actor, 65 | method_hash!("Constructor"), 66 | Some(params), 67 | ); 68 | assert!( 69 | ret_val.msg_receipt.exit_code.is_success(), 70 | "token constructor returned {ret_val:#?}", 71 | ); 72 | } 73 | 74 | // construct actor 75 | { 76 | let ret_val = 77 | tester.call_method(operator[0].1, frc46_test_actor, method_hash!("Constructor"), None); 78 | assert!(ret_val.msg_receipt.exit_code.is_success()); 79 | } 80 | 81 | // TEST: mint to test actor who rejects hook 82 | { 83 | let ret_val = tester.mint_tokens( 84 | operator[0].1, 85 | token_actor, 86 | frc46_test_actor, 87 | TokenAmount::from_atto(100), 88 | action(TestAction::Reject), 89 | ); 90 | assert!(!ret_val.msg_receipt.exit_code.is_success()); 91 | 92 | // check balance of test actor, should be zero 93 | tester.assert_token_balance_zero(operator[0].1, token_actor, frc46_test_actor); 94 | } 95 | 96 | // TEST: mint to self (token actor), should be rejected 97 | { 98 | let ret_val = tester.mint_tokens( 99 | operator[0].1, 100 | token_actor, 101 | token_actor, 102 | TokenAmount::from_atto(100), 103 | action(TestAction::Reject), 104 | ); 105 | // should fail because the token actor has no receiver hook 106 | assert!(!ret_val.msg_receipt.exit_code.is_success()); 107 | } 108 | 109 | // TEST: mint to test actor, hook burns tokens immediately 110 | { 111 | let ret_val = tester.mint_tokens_ok( 112 | operator[0].1, 113 | token_actor, 114 | frc46_test_actor, 115 | TokenAmount::from_atto(100), 116 | action(TestAction::Burn), 117 | ); 118 | let mint_result: MintReturn = ret_val.msg_receipt.return_data.deserialize().unwrap(); 119 | // tokens were burned so supply reduces back to zero 120 | assert_eq!(mint_result.supply, TokenAmount::from_atto(0)); 121 | 122 | // check balance of test actor, should also be zero 123 | tester.assert_token_balance_zero(operator[0].1, token_actor, frc46_test_actor); 124 | } 125 | 126 | // TEST: test actor transfers to self (zero amount) 127 | { 128 | let test_action = ActionParams { 129 | token_address: token_actor, 130 | action: TestAction::Transfer(frc46_test_actor, action(TestAction::Accept)), 131 | }; 132 | let params = RawBytes::serialize(test_action).unwrap(); 133 | tester.call_method_ok( 134 | operator[0].1, 135 | frc46_test_actor, 136 | method_hash!("Action"), 137 | Some(params), 138 | ); 139 | 140 | // balance should remain zero 141 | tester.assert_token_balance_zero(operator[0].1, token_actor, frc46_test_actor); 142 | } 143 | 144 | // SETUP: we need a balance on the test actor for the next few tests 145 | { 146 | let ret_val = tester.mint_tokens_ok( 147 | operator[0].1, 148 | token_actor, 149 | frc46_test_actor, 150 | TokenAmount::from_atto(100), 151 | action(TestAction::Accept), 152 | ); 153 | let mint_result: MintReturn = ret_val.msg_receipt.return_data.deserialize().unwrap(); 154 | assert_eq!(mint_result.supply, TokenAmount::from_atto(100)); 155 | tester.assert_token_balance( 156 | operator[0].1, 157 | token_actor, 158 | frc46_test_actor, 159 | TokenAmount::from_atto(100), 160 | ); 161 | } 162 | 163 | // TEST: test actor transfers back to token actor (rejected, token actor has no hook) 164 | { 165 | let test_action = ActionParams { 166 | token_address: token_actor, 167 | action: TestAction::Transfer(token_actor, RawBytes::default()), 168 | }; 169 | let params = RawBytes::serialize(test_action).unwrap(); 170 | let ret_val = tester.call_method_ok( 171 | operator[0].1, 172 | frc46_test_actor, 173 | method_hash!("Action"), 174 | Some(params), 175 | ); 176 | // action call should succeed, we'll dig into the return data to see the transfer call failure 177 | 178 | // return data is the Receipt from calling Transfer, which should show failure 179 | let receipt: Receipt = ret_val.msg_receipt.return_data.deserialize().unwrap(); 180 | assert!(!receipt.exit_code.is_success()); 181 | // check that our test actor balance hasn't changed 182 | tester.assert_token_balance( 183 | operator[0].1, 184 | token_actor, 185 | frc46_test_actor, 186 | TokenAmount::from_atto(100), 187 | ); 188 | } 189 | 190 | // TEST: test actor transfers to self (non-zero amount) 191 | { 192 | let test_action = ActionParams { 193 | token_address: token_actor, 194 | action: TestAction::Transfer(frc46_test_actor, action(TestAction::Accept)), 195 | }; 196 | let params = RawBytes::serialize(test_action).unwrap(); 197 | tester.call_method_ok( 198 | operator[0].1, 199 | frc46_test_actor, 200 | method_hash!("Action"), 201 | Some(params), 202 | ); 203 | 204 | // check that our test actor balance hasn't changed 205 | tester.assert_token_balance( 206 | operator[0].1, 207 | token_actor, 208 | frc46_test_actor, 209 | TokenAmount::from_atto(100), 210 | ); 211 | } 212 | 213 | // TEST: test actor transfers to self (non-zero amount) and rejects 214 | { 215 | let test_action = ActionParams { 216 | token_address: token_actor, 217 | action: TestAction::Transfer(frc46_test_actor, action(TestAction::Reject)), 218 | }; 219 | let params = RawBytes::serialize(test_action).unwrap(); 220 | tester.call_method_ok( 221 | operator[0].1, 222 | frc46_test_actor, 223 | method_hash!("Action"), 224 | Some(params), 225 | ); 226 | 227 | // check that our test actor balance hasn't changed 228 | tester.assert_token_balance( 229 | operator[0].1, 230 | token_actor, 231 | frc46_test_actor, 232 | TokenAmount::from_atto(100), 233 | ); 234 | } 235 | } 236 | 237 | // These types have been copied from frc46_test_actor as they can't be included into rust code from a cdylib 238 | #[derive(Serialize, Deserialize, Debug)] 239 | pub enum TestAction { 240 | Accept, 241 | Reject, 242 | Transfer(Address, RawBytes), 243 | Burn, 244 | ActionThenAbort(RawBytes), 245 | TransferWithFallback { to: Address, instructions: RawBytes, fallback: RawBytes }, 246 | } 247 | 248 | #[derive(Serialize_tuple, Deserialize_tuple, Debug)] 249 | pub struct ActionParams { 250 | pub token_address: Address, 251 | pub action: TestAction, 252 | } 253 | 254 | pub fn action(action: TestAction) -> RawBytes { 255 | RawBytes::serialize(action).unwrap() 256 | } 257 | -------------------------------------------------------------------------------- /testing/integration/tests/frc46_tokens.rs: -------------------------------------------------------------------------------- 1 | use cid::Cid; 2 | use frc42_dispatch::method_hash; 3 | use frc46_token::token::{state::TokenState, types::MintReturn}; 4 | use fvm::executor::{ApplyKind, Executor}; 5 | use fvm_integration_tests::dummy::DummyExterns; 6 | use fvm_integration_tests::tester::Account; 7 | use fvm_ipld_blockstore::MemoryBlockstore; 8 | use fvm_ipld_encoding::tuple::*; 9 | use fvm_ipld_encoding::RawBytes; 10 | use fvm_shared::address::Address; 11 | use fvm_shared::bigint::Zero; 12 | use fvm_shared::econ::TokenAmount; 13 | use fvm_shared::message::Message; 14 | use helix_test_actors::BASIC_RECEIVING_ACTOR_BINARY; 15 | use helix_test_actors::BASIC_TOKEN_ACTOR_BINARY; 16 | 17 | mod common; 18 | use common::construct_tester; 19 | 20 | // Duplicated type from basic_token_actor 21 | #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug)] 22 | pub struct MintParams { 23 | pub initial_owner: Address, 24 | pub amount: TokenAmount, 25 | pub operator_data: RawBytes, 26 | } 27 | 28 | #[test] 29 | fn it_mints_tokens() { 30 | let blockstore = MemoryBlockstore::default(); 31 | let mut tester = construct_tester(&blockstore); 32 | 33 | let minter: [Account; 1] = tester.create_accounts().unwrap(); 34 | 35 | // Set actor state 36 | let actor_state = TokenState::new(&blockstore).unwrap(); // TODO: this should probably not be exported from the package 37 | let state_cid = tester.set_state(&actor_state).unwrap(); 38 | 39 | let actor_address = Address::new_id(10000); 40 | let receive_address = Address::new_id(10010); 41 | tester 42 | .set_actor_from_bin(BASIC_TOKEN_ACTOR_BINARY, state_cid, actor_address, TokenAmount::zero()) 43 | .unwrap(); 44 | tester 45 | .set_actor_from_bin( 46 | BASIC_RECEIVING_ACTOR_BINARY, 47 | Cid::default(), 48 | receive_address, 49 | TokenAmount::zero(), 50 | ) 51 | .unwrap(); 52 | 53 | // Instantiate machine 54 | tester.instantiate_machine(DummyExterns).unwrap(); 55 | 56 | // Helper to simplify sending messages 57 | let mut sequence = 0u64; 58 | let mut call_method = |from, to, method_num, params: Option| { 59 | let message = Message { 60 | from, 61 | to, 62 | gas_limit: 99999999, 63 | method_num, 64 | sequence, 65 | params: params.unwrap_or_default(), 66 | ..Message::default() 67 | }; 68 | sequence += 1; 69 | tester 70 | .executor 71 | .as_mut() 72 | .unwrap() 73 | .execute_message(message, ApplyKind::Explicit, 100) 74 | .unwrap() 75 | }; 76 | 77 | // Construct the token actor 78 | let ret_val = call_method(minter[0].1, actor_address, method_hash!("Constructor"), None); 79 | println!("token actor constructor return data: {:#?}", &ret_val); 80 | 81 | let ret_val = call_method(minter[0].1, receive_address, method_hash!("Constructor"), None); 82 | println!("receiving actor constructor return data: {:#?}", &ret_val); 83 | 84 | // Mint some tokens 85 | let mint_params = MintParams { 86 | initial_owner: receive_address, 87 | amount: TokenAmount::from_atto(100), 88 | operator_data: RawBytes::default(), 89 | }; 90 | let params = RawBytes::serialize(mint_params).unwrap(); 91 | let ret_val = call_method(minter[0].1, actor_address, method_hash!("Mint"), Some(params)); 92 | println!("mint return data {:#?}", &ret_val); 93 | let return_data = ret_val.msg_receipt.return_data; 94 | if return_data.is_empty() { 95 | println!("return data was empty"); 96 | } else { 97 | let mint_result: MintReturn = return_data.deserialize().unwrap(); 98 | println!("new total supply: {:?}", &mint_result.supply); 99 | } 100 | 101 | // Check balance 102 | let params = RawBytes::serialize(receive_address).unwrap(); 103 | let ret_val = call_method(minter[0].1, actor_address, method_hash!("BalanceOf"), Some(params)); 104 | println!("balance return data {:#?}", &ret_val); 105 | 106 | let return_data = ret_val.msg_receipt.return_data; 107 | let balance: TokenAmount = return_data.deserialize().unwrap(); 108 | println!("balance: {balance:?}"); 109 | } 110 | -------------------------------------------------------------------------------- /testing/integration/tests/frc53_nfts.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::method_hash; 2 | use frc53_nft::types::{ListTokensParams, ListTokensReturn}; 3 | use frc53_nft::{types::MintReturn, types::TokenID}; 4 | use fvm_integration_tests::{dummy::DummyExterns, tester::Account}; 5 | use fvm_ipld_bitfield::bitfield; 6 | use fvm_ipld_blockstore::MemoryBlockstore; 7 | use fvm_ipld_encoding::RawBytes; 8 | 9 | mod common; 10 | use common::frc53_nft_helpers::{MintParams, NFTHelper}; 11 | use common::{construct_tester, TestHelpers}; 12 | use helix_test_actors::{BASIC_NFT_ACTOR_BINARY, BASIC_RECEIVING_ACTOR_BINARY}; 13 | 14 | #[test] 15 | fn test_nft_actor() { 16 | let blockstore = MemoryBlockstore::default(); 17 | let mut tester = construct_tester(&blockstore); 18 | let minter: [Account; 1] = tester.create_accounts().unwrap(); 19 | 20 | let actor_address = tester.install_actor_stateless(BASIC_NFT_ACTOR_BINARY, 10_000); 21 | let receiver_address = tester.install_actor_stateless(BASIC_RECEIVING_ACTOR_BINARY, 10_001); 22 | 23 | // Instantiate machine 24 | tester.instantiate_machine(DummyExterns).unwrap(); 25 | 26 | // Construct the token actor 27 | tester.call_method_ok(minter[0].1, actor_address, method_hash!("Constructor"), None); 28 | tester.call_method_ok(minter[0].1, receiver_address, method_hash!("Constructor"), None); 29 | 30 | { 31 | // Mint a single token 32 | let mint_params = MintParams { 33 | initial_owner: receiver_address, 34 | metadata: vec![String::from("metadata")], 35 | operator_data: RawBytes::default(), 36 | }; 37 | let mint_params = RawBytes::serialize(mint_params).unwrap(); 38 | let ret_val = tester.call_method_ok( 39 | minter[0].1, 40 | actor_address, 41 | method_hash!("Mint"), 42 | Some(mint_params), 43 | ); 44 | let mint_result = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 45 | assert_eq!(mint_result.token_ids, vec![0]); 46 | assert_eq!(mint_result.balance, 1); 47 | assert_eq!(mint_result.supply, 1); 48 | 49 | // Check the total supply increased 50 | let ret_val = 51 | tester.call_method_ok(minter[0].1, actor_address, method_hash!("TotalSupply"), None); 52 | let total_supply = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 53 | assert_eq!(total_supply, 1); 54 | 55 | // Check the balance is correct 56 | tester.assert_nft_balance(minter[0].1, actor_address, receiver_address, 1); 57 | // Check the owner is correct 58 | tester.assert_nft_owner(minter[0].1, actor_address, 0, receiver_address.id().unwrap()); 59 | // Check metatdata is correct 60 | tester.assert_nft_metadata(minter[0].1, actor_address, 0, "metadata".into()) 61 | } 62 | 63 | { 64 | // Mint a second token 65 | let mint_params = MintParams { 66 | initial_owner: receiver_address, 67 | metadata: vec![String::from("metadata2")], 68 | operator_data: RawBytes::default(), 69 | }; 70 | let mint_params = RawBytes::serialize(mint_params).unwrap(); 71 | let ret_val = tester.call_method_ok( 72 | minter[0].1, 73 | actor_address, 74 | method_hash!("Mint"), 75 | Some(mint_params), 76 | ); 77 | let mint_result = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 78 | assert_eq!(mint_result.token_ids, vec![1]); 79 | assert_eq!(mint_result.balance, 2); 80 | assert_eq!(mint_result.supply, 2); 81 | 82 | // Check the total supply increased 83 | let ret_val = 84 | tester.call_method_ok(minter[0].1, actor_address, method_hash!("TotalSupply"), None); 85 | let total_supply = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 86 | assert_eq!(total_supply, 2); 87 | 88 | // Check the balance increased 89 | tester.assert_nft_balance(minter[0].1, actor_address, receiver_address, 2); 90 | // Check the owner is correct 91 | tester.assert_nft_owner(minter[0].1, actor_address, 1, receiver_address.id().unwrap()); 92 | // Check metatdata is correct 93 | tester.assert_nft_metadata(minter[0].1, actor_address, 1, "metadata2".into()) 94 | } 95 | 96 | { 97 | // Attempt to burn a non-existent token 98 | let burn_params: Vec = vec![100]; 99 | let burn_params = RawBytes::serialize(burn_params).unwrap(); 100 | let ret_val = 101 | tester.call_method(minter[0].1, actor_address, method_hash!("Burn"), Some(burn_params)); 102 | // call should fail 103 | assert!(!ret_val.msg_receipt.exit_code.is_success(), "{ret_val:#?}"); 104 | 105 | // Check the total supply didn't change 106 | let ret_val = 107 | tester.call_method_ok(minter[0].1, actor_address, method_hash!("TotalSupply"), None); 108 | let total_supply = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 109 | assert_eq!(total_supply, 2); 110 | 111 | // Check the balance didn't change 112 | tester.assert_nft_balance(minter[0].1, actor_address, receiver_address, 2); 113 | } 114 | 115 | { 116 | // Attempt to burn the correct token but without permission 117 | let burn_params: Vec = vec![0]; 118 | let burn_params = RawBytes::serialize(burn_params).unwrap(); 119 | let ret_val = 120 | tester.call_method(minter[0].1, actor_address, method_hash!("Burn"), Some(burn_params)); 121 | assert!(!ret_val.msg_receipt.exit_code.is_success(), "{ret_val:#?}"); 122 | 123 | // Check the total supply didn't change 124 | let ret_val = 125 | tester.call_method_ok(minter[0].1, actor_address, method_hash!("TotalSupply"), None); 126 | let total_supply = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 127 | assert_eq!(total_supply, 2); 128 | 129 | // Check the balance didn't change 130 | tester.assert_nft_balance(minter[0].1, actor_address, receiver_address, 2); 131 | } 132 | 133 | { 134 | // Minting multiple tokens produces sequential ids 135 | let mint_params = MintParams { 136 | initial_owner: receiver_address, 137 | metadata: vec![String::default(), String::default()], 138 | operator_data: RawBytes::default(), 139 | }; 140 | let mint_params = RawBytes::serialize(mint_params).unwrap(); 141 | let ret_val = 142 | tester.call_method(minter[0].1, actor_address, method_hash!("Mint"), Some(mint_params)); 143 | assert!(ret_val.msg_receipt.exit_code.is_success(), "{ret_val:#?}"); 144 | let mint_result = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 145 | assert_eq!(mint_result.token_ids, vec![2, 3]); 146 | assert_eq!(mint_result.balance, 4); 147 | assert_eq!(mint_result.supply, 4); 148 | 149 | // Check the total supply increased by two 150 | let ret_val = 151 | tester.call_method(minter[0].1, actor_address, method_hash!("TotalSupply"), None); 152 | assert!(ret_val.msg_receipt.exit_code.is_success(), "{ret_val:#?}"); 153 | let total_supply = ret_val.msg_receipt.return_data.deserialize::().unwrap(); 154 | // Check the owner is correct 155 | tester.assert_nft_owner(minter[0].1, actor_address, 2, receiver_address.id().unwrap()); 156 | tester.assert_nft_owner(minter[0].1, actor_address, 3, receiver_address.id().unwrap()); 157 | assert_eq!(total_supply, 4); 158 | } 159 | 160 | // List all the tokens 161 | { 162 | let list_tokens_params = ListTokensParams { cursor: RawBytes::default(), limit: u64::MAX }; 163 | let list_tokens_params = RawBytes::serialize(list_tokens_params).unwrap(); 164 | let ret_val = tester.call_method_ok( 165 | minter[0].1, 166 | actor_address, 167 | method_hash!("ListTokens"), 168 | Some(list_tokens_params), 169 | ); 170 | let list_tokens_result = 171 | ret_val.msg_receipt.return_data.deserialize::().unwrap(); 172 | assert_eq!(list_tokens_result.tokens, bitfield![1, 1, 1, 1]); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /testing/integration/tests/transfer_tokens.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::method_hash; 2 | use frc46_token::token::{state::TokenState, types::MintReturn}; 3 | use fvm_integration_tests::{dummy::DummyExterns, tester::Account}; 4 | use fvm_ipld_blockstore::MemoryBlockstore; 5 | use fvm_ipld_encoding::{tuple::*, RawBytes}; 6 | use fvm_shared::address::Address; 7 | use fvm_shared::econ::TokenAmount; 8 | 9 | mod common; 10 | use common::frc46_token_helpers::TokenHelper; 11 | use common::{construct_tester, TestHelpers}; 12 | use helix_test_actors::{ 13 | BASIC_RECEIVING_ACTOR_BINARY, BASIC_TOKEN_ACTOR_BINARY, BASIC_TRANSFER_ACTOR_BINARY, 14 | }; 15 | 16 | #[derive(Serialize_tuple, Deserialize_tuple)] 17 | struct TransferActorState { 18 | operator_address: Option
, 19 | token_address: Option
, 20 | } 21 | 22 | #[test] 23 | fn transfer_tokens() { 24 | let blockstore = MemoryBlockstore::default(); 25 | let mut tester = construct_tester(&blockstore); 26 | 27 | let operator: [Account; 1] = tester.create_accounts().unwrap(); 28 | 29 | let token_state = TokenState::new(&blockstore).unwrap(); 30 | let transfer_state = TransferActorState { operator_address: None, token_address: None }; 31 | 32 | let token_address = 33 | tester.install_actor_with_state(BASIC_TOKEN_ACTOR_BINARY, 10000, token_state); 34 | let transfer_address = 35 | tester.install_actor_with_state(BASIC_TRANSFER_ACTOR_BINARY, 10010, transfer_state); 36 | let receiver_address = tester.install_actor_stateless(BASIC_RECEIVING_ACTOR_BINARY, 10020); 37 | 38 | // Instantiate machine 39 | tester.instantiate_machine(DummyExterns).unwrap(); 40 | 41 | // construct actors 42 | for actor in [token_address, transfer_address, receiver_address] { 43 | let ret_val = tester.call_method(operator[0].1, actor, method_hash!("Constructor"), None); 44 | assert!(ret_val.msg_receipt.exit_code.is_success()); 45 | } 46 | 47 | // mint some tokens 48 | let ret_val = tester.mint_tokens( 49 | operator[0].1, 50 | token_address, 51 | transfer_address, 52 | TokenAmount::from_atto(100), 53 | RawBytes::default(), 54 | ); 55 | println!("minting return data {:#?}", &ret_val); 56 | let mint_result: MintReturn = ret_val.msg_receipt.return_data.deserialize().unwrap(); 57 | println!("minted - total supply: {:?}", &mint_result.supply); 58 | assert_eq!(mint_result.supply, TokenAmount::from_atto(100)); 59 | 60 | // check balance of transfer actor 61 | let balance = tester.token_balance(operator[0].1, token_address, transfer_address); 62 | println!("balance held by transfer actor: {balance:?}"); 63 | assert_eq!(balance, TokenAmount::from_atto(100)); 64 | 65 | // forward from transfer to receiving actor 66 | let params = RawBytes::serialize(receiver_address).unwrap(); 67 | let ret_val = 68 | tester.call_method(operator[0].1, transfer_address, method_hash!("Forward"), Some(params)); 69 | println!("forwarding return data {:#?}", &ret_val); 70 | 71 | // check balance of receiver actor 72 | let balance = tester.token_balance(operator[0].1, token_address, transfer_address); 73 | println!("balance held by transfer actor: {balance:?}"); 74 | assert_eq!(balance, TokenAmount::from_atto(0)); 75 | 76 | let balance = tester.token_balance(operator[0].1, token_address, receiver_address); 77 | println!("balance held by receiver actor: {balance:?}"); 78 | assert_eq!(balance, TokenAmount::from_atto(100)); 79 | } 80 | -------------------------------------------------------------------------------- /testing/test_actors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helix_test_actors" 3 | description = "WASM actor" 4 | version = "0.1.0" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_nft_actor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic_nft_actor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | frc42_dispatch = { workspace = true } 9 | frc53_nft = { workspace = true } 10 | fvm_actor_utils = { workspace = true } 11 | 12 | cid = { workspace = true } 13 | fvm_ipld_blockstore = { workspace = true } 14 | fvm_ipld_encoding = { workspace = true } 15 | fvm_sdk = { workspace = true } 16 | fvm_shared = { workspace = true } 17 | serde = { workspace = true } 18 | thiserror = { workspace = true } 19 | 20 | [lib] 21 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 22 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_nft_actor/README.md: -------------------------------------------------------------------------------- 1 | # Basic NFT Actor 2 | 3 | This is an **example** that uses the 4 | [frc53_nft](../../../../frc53_nft/README.md) package to implement a 5 | [FRC-0053-compliant](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0053.md) 6 | token actor. This actor **should not be used in production** as it has a 7 | faucet-like minting strategy. 8 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_nft_actor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::match_method; 2 | use frc53_nft::{ 3 | state::NFTState, 4 | types::{ 5 | ApproveForAllParams, ApproveParams, BurnFromParams, ListAccountOperatorsParams, 6 | ListOperatorTokensParams, ListOwnedTokensParams, ListTokenOperatorsParams, 7 | ListTokensParams, RevokeForAllParams, RevokeParams, TokenID, TransferFromParams, 8 | TransferParams, 9 | }, 10 | NFT, 11 | }; 12 | use fvm_actor_utils::{ 13 | blockstore::Blockstore, messaging::FvmMessenger, syscalls::fvm_syscalls::FvmSyscalls, 14 | util::ActorRuntime, 15 | }; 16 | use fvm_ipld_encoding::{de::DeserializeOwned, ser, tuple::*, RawBytes, DAG_CBOR}; 17 | use fvm_sdk as sdk; 18 | use fvm_shared::address::Address; 19 | use fvm_shared::error::ExitCode; 20 | use sdk::{sys::ErrorNumber, NO_DATA_BLOCK_ID}; 21 | use thiserror::Error; 22 | 23 | #[no_mangle] 24 | fn invoke(params: u32) -> u32 { 25 | let method_num = sdk::message::method_number(); 26 | 27 | if method_num == 1 { 28 | constructor(); 29 | return NO_DATA_BLOCK_ID; 30 | } 31 | 32 | // After constructor has run we have state 33 | let messenger = FvmMessenger::default(); 34 | let root_cid = sdk::sself::root().unwrap(); 35 | let helpers = ActorRuntime::::new_fvm_runtime(); 36 | let mut state = NFTState::load(&helpers, &root_cid).unwrap(); 37 | let mut handle = NFT::wrap(helpers, &mut state); 38 | 39 | match_method!(method_num,{ 40 | "BalanceOf" => { 41 | let params = deserialize_params::
(params); 42 | let res = handle.balance_of(¶ms).unwrap(); 43 | return_ipld(&res).unwrap() 44 | } 45 | "TotalSupply" => { 46 | let res = handle.total_supply(); 47 | return_ipld(&res).unwrap() 48 | } 49 | "OwnerOf" => { 50 | let params = deserialize_params::(params); 51 | let res = handle.owner_of(params).unwrap(); 52 | return_ipld(&res).unwrap() 53 | } 54 | "Metadata" => { 55 | let params = deserialize_params::(params); 56 | let res = handle.metadata(params).unwrap(); 57 | return_ipld(&res).unwrap() 58 | } 59 | "Mint" => { 60 | let params = deserialize_params::(params); 61 | let caller = Address::new_id(sdk::message::caller()); 62 | let mut hook = handle.mint(&caller, ¶ms.initial_owner, params.metadata, params.operator_data, RawBytes::default()).unwrap(); 63 | 64 | let cid = handle.flush().unwrap(); 65 | sdk::sself::set_root(&cid).unwrap(); 66 | 67 | let hook_res = hook.call(&messenger).unwrap(); 68 | 69 | let ret_val = handle.mint_return(hook_res, cid).unwrap(); 70 | return_ipld(&ret_val).unwrap() 71 | } 72 | "Transfer" => { 73 | let params = deserialize_params::(params); 74 | let mut hook = handle.transfer( 75 | &caller_address(), 76 | ¶ms.to, 77 | ¶ms.token_ids, 78 | params.operator_data, 79 | RawBytes::default() 80 | ).unwrap(); 81 | 82 | let cid = handle.flush().unwrap(); 83 | sdk::sself::set_root(&cid).unwrap(); 84 | 85 | let hook_res = hook.call(&messenger).unwrap(); 86 | 87 | let ret_val = handle.transfer_return(hook_res, cid).unwrap(); 88 | return_ipld(&ret_val).unwrap() 89 | } 90 | "TransferFrom" => { 91 | let params = deserialize_params::(params); 92 | let mut hook = handle.transfer_from( 93 | &caller_address(), 94 | ¶ms.from, 95 | ¶ms.to, 96 | ¶ms.token_ids, 97 | params.operator_data, 98 | RawBytes::default() 99 | ).unwrap(); 100 | 101 | let cid = handle.flush().unwrap(); 102 | sdk::sself::set_root(&cid).unwrap(); 103 | 104 | let hook_res = hook.call(&messenger).unwrap(); 105 | 106 | let ret_val = handle.transfer_from_return(hook_res, cid).unwrap(); 107 | return_ipld(&ret_val).unwrap() 108 | } 109 | "Burn" => { 110 | let params = deserialize_params::>(params); 111 | let caller = sdk::message::caller(); 112 | let ret_val = handle.burn(&Address::new_id(caller), ¶ms).unwrap(); 113 | 114 | let cid = handle.flush().unwrap(); 115 | sdk::sself::set_root(&cid).unwrap(); 116 | return_ipld(&ret_val).unwrap() 117 | } 118 | "BurnFrom" => { 119 | let params = deserialize_params::(params); 120 | let caller = sdk::message::caller(); 121 | handle.burn_from(¶ms.from, &Address::new_id(caller), ¶ms.token_ids).unwrap(); 122 | 123 | let cid = handle.flush().unwrap(); 124 | sdk::sself::set_root(&cid).unwrap(); 125 | NO_DATA_BLOCK_ID 126 | } 127 | "Approve" => { 128 | let params = deserialize_params::(params); 129 | handle.approve(&caller_address(), ¶ms.operator, ¶ms.token_ids).unwrap(); 130 | let cid = handle.flush().unwrap(); 131 | sdk::sself::set_root(&cid).unwrap(); 132 | NO_DATA_BLOCK_ID 133 | } 134 | "Revoke" => { 135 | let params = deserialize_params::(params); 136 | handle.revoke(&caller_address(), ¶ms.operator, ¶ms.token_ids).unwrap(); 137 | let cid = handle.flush().unwrap(); 138 | sdk::sself::set_root(&cid).unwrap(); 139 | NO_DATA_BLOCK_ID 140 | } 141 | "ApproveForAll" => { 142 | let params = deserialize_params::(params); 143 | handle.approve_for_owner(&caller_address(), ¶ms.operator).unwrap(); 144 | let cid = handle.flush().unwrap(); 145 | sdk::sself::set_root(&cid).unwrap(); 146 | NO_DATA_BLOCK_ID 147 | } 148 | "RevokeForAll" => { 149 | let params = deserialize_params::(params); 150 | handle.revoke_for_all(&caller_address(), ¶ms.operator).unwrap(); 151 | let cid = handle.flush().unwrap(); 152 | sdk::sself::set_root(&cid).unwrap(); 153 | NO_DATA_BLOCK_ID 154 | } 155 | "ListTokens" => { 156 | let params = deserialize_params::(params); 157 | let res = handle.list_tokens(params.cursor, params.limit).unwrap(); 158 | return_ipld(&res).unwrap() 159 | } 160 | "ListOwnedTokens" => { 161 | let params = deserialize_params::(params); 162 | let res = handle.list_owned_tokens(¶ms.owner, params.cursor, params.limit).unwrap(); 163 | return_ipld(&res).unwrap() 164 | } 165 | "ListTokenOperators" => { 166 | let params = deserialize_params::(params); 167 | let res = handle.list_token_operators(params.token_id, params.cursor, params.limit).unwrap(); 168 | return_ipld(&res).unwrap() 169 | } 170 | "ListOperatorTokens" => { 171 | let params = deserialize_params::(params); 172 | let res = handle.list_operator_tokens(¶ms.operator, params.cursor, params.limit).unwrap(); 173 | return_ipld(&res).unwrap() 174 | } 175 | "ListAccountOperators" => { 176 | let params = deserialize_params::(params); 177 | let res = handle.list_account_operators(¶ms.owner, params.cursor, params.limit).unwrap(); 178 | return_ipld(&res).unwrap() 179 | } 180 | _ => { 181 | sdk::vm::abort(ExitCode::USR_ILLEGAL_ARGUMENT.value(), Some(&format!("Unknown method number {method_num:?} was invoked"))); 182 | } 183 | }) 184 | } 185 | 186 | pub fn constructor() { 187 | let bs = Blockstore {}; 188 | let nft_state = NFTState::new(&bs).unwrap(); 189 | let state_cid = nft_state.save(&bs).unwrap(); 190 | sdk::sself::set_root(&state_cid).unwrap(); 191 | } 192 | 193 | // Note that the below MintParams needs to be manually synced with 194 | // testing/fil_token_integration/tests/frc53_nfts.rs::MintParams 195 | 196 | /// Minting tokens goes directly to the caller for now. 197 | #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] 198 | pub struct MintParams { 199 | pub initial_owner: Address, 200 | pub metadata: Vec, 201 | pub operator_data: RawBytes, 202 | } 203 | 204 | /// Grab the incoming parameters and convert from RawBytes to deserialized struct. 205 | pub fn deserialize_params(params: u32) -> O { 206 | let params = sdk::message::params_raw(params).unwrap().unwrap(); 207 | let params = RawBytes::new(params.data); 208 | params.deserialize().unwrap() 209 | } 210 | 211 | #[derive(Error, Debug)] 212 | enum IpldError { 213 | #[error("ipld encoding error: {0}")] 214 | Encoding(#[from] fvm_ipld_encoding::Error), 215 | #[error("ipld blockstore error: {0}")] 216 | Blockstore(#[from] ErrorNumber), 217 | } 218 | 219 | fn return_ipld(value: &T) -> std::result::Result 220 | where 221 | T: ser::Serialize + ?Sized, 222 | { 223 | let bytes = fvm_ipld_encoding::to_vec(value)?; 224 | Ok(sdk::ipld::put_block(DAG_CBOR, bytes.as_slice())?) 225 | } 226 | 227 | fn caller_address() -> Address { 228 | Address::new_id(sdk::message::caller()) 229 | } 230 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_receiving_actor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic_receiving_actor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | frc42_dispatch = { workspace = true } 9 | frc46_token = { workspace = true } 10 | frc53_nft = { workspace = true } 11 | fvm_actor_utils = { workspace = true } 12 | 13 | fvm_ipld_blockstore = { workspace = true } 14 | fvm_ipld_encoding = { workspace = true } 15 | fvm_sdk = { workspace = true } 16 | fvm_shared = { workspace = true } 17 | 18 | [lib] 19 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 20 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_receiving_actor/README.md: -------------------------------------------------------------------------------- 1 | # Basic Token Receiver 2 | 3 | This is an **example** that uses the 4 | [frc46_token](../../../../frc46_token/README.md) package to implement a 5 | [FRC-0046-compliant](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md) 6 | universal receiver actor. This actor inspects the `type` field and rejects 7 | incoming transfers if the token is not of type FRC46. 8 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_receiving_actor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::match_method; 2 | use frc46_token::receiver::{FRC46TokenReceived, FRC46_TOKEN_TYPE}; 3 | use frc53_nft::receiver::{FRC53TokenReceived, FRC53_TOKEN_TYPE}; 4 | use fvm_actor_utils::receiver::UniversalReceiverParams; 5 | use fvm_ipld_encoding::{de::DeserializeOwned, RawBytes}; 6 | use fvm_sdk as sdk; 7 | use fvm_shared::error::ExitCode; 8 | use sdk::NO_DATA_BLOCK_ID; 9 | 10 | /// Grab the incoming parameters and convert from RawBytes to deserialized struct. 11 | pub fn deserialize_params(params: u32) -> O { 12 | let params = sdk::message::params_raw(params).unwrap().unwrap(); 13 | let params = RawBytes::new(params.data); 14 | params.deserialize().unwrap() 15 | } 16 | 17 | #[no_mangle] 18 | fn invoke(input: u32) -> u32 { 19 | std::panic::set_hook(Box::new(|info| { 20 | sdk::vm::abort(ExitCode::USR_ASSERTION_FAILED.value(), Some(&format!("{info}"))) 21 | })); 22 | let method_num = sdk::message::method_number(); 23 | match_method!(method_num, { 24 | "Constructor" => { 25 | // this is a stateless actor so constructor does nothing 26 | NO_DATA_BLOCK_ID 27 | }, 28 | "Receive" => { 29 | // Receive is passed a UniversalReceiverParams 30 | let params: UniversalReceiverParams = deserialize_params(input); 31 | 32 | // reject if not an FRC46 token or an FRC53 NFT 33 | // we don't know how to inspect other payloads in this example 34 | match params.type_ { 35 | FRC46_TOKEN_TYPE => { 36 | // get token transfer data 37 | let _token_params: FRC46TokenReceived = params.payload.deserialize().unwrap(); 38 | // TODO: inspect token_params and decide if we'll accept the transfer 39 | // to reject it, we just abort (or panic, which does the same thing) 40 | } 41 | FRC53_TOKEN_TYPE => { 42 | // get token transfer data 43 | let _token_params: FRC53TokenReceived = params.payload.deserialize().unwrap(); 44 | // TODO: inspect token_params and decide if we'll accept the transfer 45 | // to reject it, we just abort (or panic, which does the same thing) 46 | } 47 | _ => { 48 | panic!("invalid token type, rejecting transfer"); 49 | } 50 | } 51 | 52 | NO_DATA_BLOCK_ID 53 | }, 54 | _ => { 55 | sdk::vm::abort( 56 | ExitCode::USR_UNHANDLED_MESSAGE.value(), 57 | Some("Unknown method number"), 58 | ); 59 | } 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_token_actor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic_token_actor" 3 | version = "0.1.0" 4 | repository = "https://github.com/helix-collective/filecoin" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | frc46_token = { path = "../../../../frc46_token" } 10 | fvm_actor_utils = { path = "../../../../fvm_actor_utils" } 11 | 12 | cid = { workspace = true } 13 | fvm_ipld_blockstore = { workspace = true } 14 | fvm_ipld_encoding = { workspace = true } 15 | fvm_sdk = { workspace = true } 16 | fvm_shared = { workspace = true } 17 | num-traits = { version = "0.2.19" } 18 | serde = { version = "1.0.219", features = ["derive"] } 19 | thiserror = { version = "2.0.12" } 20 | 21 | [lib] 22 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 23 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_token_actor/README.md: -------------------------------------------------------------------------------- 1 | # Basic Token Actor 2 | 3 | This is an **example** that uses the 4 | [frc46_token](../../../../frc46_token/README.md) package to implement a 5 | [FRC-0046-compliant](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md) 6 | token actor. This actor **should not be used in production** as it has a 7 | faucet-like minting strategy. 8 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_token_actor/src/util.rs: -------------------------------------------------------------------------------- 1 | use frc46_token::token::TokenError; 2 | use fvm_actor_utils::receiver::ReceiverHookError; 3 | use fvm_ipld_encoding::{de::DeserializeOwned, RawBytes}; 4 | use fvm_sdk as sdk; 5 | use fvm_shared::address::Address; 6 | use thiserror::Error; 7 | 8 | /// Errors that can occur during the execution of this actor. 9 | #[derive(Error, Debug)] 10 | pub enum RuntimeError { 11 | /// Error from the underlying token library. 12 | #[error("error in token: {0}")] 13 | Token(#[from] TokenError), 14 | /// Error from the underlying universal receiver hook library. 15 | #[error("error calling receiver hook: {0}")] 16 | Receiver(#[from] ReceiverHookError), 17 | } 18 | 19 | pub fn caller_address() -> Address { 20 | let caller = sdk::message::caller(); 21 | Address::new_id(caller) 22 | } 23 | 24 | /// Grab the incoming parameters and convert from [`RawBytes`] to deserialized struct. 25 | pub fn deserialize_params(params: u32) -> O { 26 | let params = sdk::message::params_raw(params).unwrap().unwrap(); 27 | let params = RawBytes::new(params.data); 28 | params.deserialize().unwrap() 29 | } 30 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_transfer_actor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic_transfer_actor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | frc42_dispatch = { workspace = true } 9 | frc46_token = { workspace = true } 10 | fvm_actor_utils = { workspace = true } 11 | 12 | cid = { workspace = true } 13 | fvm_ipld_blockstore = { workspace = true } 14 | fvm_ipld_encoding = { workspace = true } 15 | fvm_sdk = { workspace = true } 16 | fvm_shared = { workspace = true } 17 | multihash-codetable = { workspace = true, features = ["blake2b"] } 18 | serde = { workspace = true } 19 | 20 | [lib] 21 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 22 | -------------------------------------------------------------------------------- /testing/test_actors/actors/basic_transfer_actor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use cid::Cid; 2 | use frc42_dispatch::{match_method, method_hash}; 3 | use frc46_token::receiver::{FRC46TokenReceived, FRC46_TOKEN_TYPE}; 4 | use frc46_token::token::types::TransferParams; 5 | use fvm_actor_utils::receiver::UniversalReceiverParams; 6 | use fvm_ipld_blockstore::Block; 7 | use fvm_ipld_encoding::ipld_block::IpldBlock; 8 | use fvm_ipld_encoding::tuple::*; 9 | use fvm_ipld_encoding::{de::DeserializeOwned, RawBytes, DAG_CBOR}; 10 | use fvm_sdk as sdk; 11 | use fvm_shared::sys::SendFlags; 12 | use fvm_shared::{address::Address, bigint::Zero, econ::TokenAmount, error::ExitCode}; 13 | use multihash_codetable::Code; 14 | use sdk::NO_DATA_BLOCK_ID; 15 | 16 | /// Grab the incoming parameters and convert from [`RawBytes`] to the deserialized struct. 17 | pub fn deserialize_params(params: u32) -> O { 18 | let params = sdk::message::params_raw(params).unwrap().unwrap(); 19 | let params = RawBytes::new(params.data); 20 | params.deserialize().unwrap() 21 | } 22 | 23 | #[derive(Serialize_tuple, Deserialize_tuple)] 24 | struct TransferActorState { 25 | operator_address: Option
, 26 | token_address: Option
, 27 | } 28 | 29 | impl TransferActorState { 30 | fn load(cid: &Cid) -> Self { 31 | let data = sdk::ipld::get(cid).unwrap(); 32 | fvm_ipld_encoding::from_slice::(&data).unwrap() 33 | } 34 | 35 | fn save(&self) -> Cid { 36 | let serialized = fvm_ipld_encoding::to_vec(self).unwrap(); 37 | let block = Block { codec: DAG_CBOR, data: serialized }; 38 | sdk::ipld::put(Code::Blake2b256.into(), 32, block.codec, block.data.as_ref()).unwrap() 39 | } 40 | } 41 | 42 | /// Implements a simple actor that can hold and transfer tokens. 43 | /// 44 | /// First operator to send it tokens will be saved and tokens from other operators will be rejected. 45 | /// 46 | /// Address of the token actor is also saved as this identifies the token type. 47 | /// 48 | /// After receiving some tokens, it does nothing until the Forward method is called by the initial 49 | /// operator. When the `Forward` method is invoked, it will transfer the entire balance it holds to 50 | /// a given address. 51 | /// 52 | /// Forward requires the same operator to initiate transfer and will abort if the operator address doesn't match, 53 | /// or if the receiver hook rejects the transfer. 54 | #[no_mangle] 55 | fn invoke(input: u32) -> u32 { 56 | std::panic::set_hook(Box::new(|info| { 57 | sdk::vm::abort(ExitCode::USR_ASSERTION_FAILED.value(), Some(&format!("{info}"))) 58 | })); 59 | 60 | let method_num = sdk::message::method_number(); 61 | match_method!(method_num, { 62 | "Constructor" => { 63 | let initial_state = TransferActorState { operator_address: None, token_address: None }; 64 | let cid = initial_state.save(); 65 | sdk::sself::set_root(&cid).unwrap(); 66 | 67 | NO_DATA_BLOCK_ID 68 | }, 69 | "Receive" => { 70 | let mut state = TransferActorState::load(&sdk::sself::root().unwrap()); 71 | // Received is passed a TokenReceivedParams 72 | let params: UniversalReceiverParams = deserialize_params(input); 73 | 74 | // reject if not an FRC46 token 75 | // we don't know how to inspect other payloads here 76 | if params.type_ != FRC46_TOKEN_TYPE { 77 | panic!("invalid token type, rejecting transfer"); 78 | } 79 | 80 | // get token transfer data 81 | let token_params: FRC46TokenReceived = params.payload.deserialize().unwrap(); 82 | 83 | // check the address, we'll remember the first operator and reject others later 84 | match state.operator_address { 85 | Some(operator) => { 86 | let actor_id = sdk::actor::resolve_address(&operator).unwrap(); 87 | if actor_id != token_params.operator { 88 | panic!("cannot accept from this operator"); 89 | } 90 | } 91 | None => { 92 | state.operator_address = Some(Address::new_id(token_params.operator)); 93 | state.token_address = Some(Address::new_id(sdk::message::caller())); 94 | let cid = state.save(); 95 | sdk::sself::set_root(&cid).unwrap(); 96 | } 97 | } 98 | 99 | // all good, don't need to return anything 100 | NO_DATA_BLOCK_ID 101 | }, 102 | "Forward" => { 103 | let state = TransferActorState::load(&sdk::sself::root().unwrap()); 104 | 105 | let target: Address = deserialize_params(input); 106 | 107 | // match sender address to the one who operated the last transfer 108 | // if there's no address set, abort because we're expecting a transfer first 109 | match state.operator_address { 110 | Some(operator) => { 111 | let actor_id = sdk::actor::resolve_address(&operator).unwrap(); 112 | if actor_id != sdk::message::caller() { 113 | panic!("cannot accept from this operator"); 114 | } 115 | } 116 | None => panic!("no operator id set"), 117 | } 118 | 119 | // get our balance 120 | let self_address = Address::new_id(sdk::message::receiver()); 121 | let balance_ret = sdk::send::send(&state.token_address.unwrap(), method_hash!("BalanceOf"), 122 | IpldBlock::serialize_cbor(&self_address).unwrap(), TokenAmount::zero(), 123 | None, 124 | SendFlags::empty()).unwrap(); 125 | if !balance_ret.exit_code.is_success() { 126 | panic!("unable to get balance"); 127 | } 128 | let balance = balance_ret.return_data.unwrap().deserialize::().unwrap(); 129 | 130 | // transfer to target address 131 | let params = TransferParams { 132 | to: target, 133 | amount: balance, // send everything 134 | operator_data: RawBytes::default(), 135 | }; 136 | let transfer_ret = sdk::send::send(&state.token_address.unwrap(), method_hash!("Transfer"), 137 | 138 | IpldBlock::serialize_cbor(¶ms).unwrap(), TokenAmount::zero(), 139 | None, 140 | SendFlags::empty(),).unwrap(); 141 | if !transfer_ret.exit_code.is_success() { 142 | panic!("transfer call failed"); 143 | } 144 | 145 | // we could return the balance sent or something like that 146 | // but the test we run from is checking that already so no need to do it here 147 | NO_DATA_BLOCK_ID 148 | } 149 | _ => { 150 | sdk::vm::abort( 151 | ExitCode::USR_UNHANDLED_MESSAGE.value(), 152 | Some("Unknown method number"), 153 | ); 154 | } 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /testing/test_actors/actors/frc46_factory_token/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frc46_factory_token" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | cid = { workspace = true } 9 | frc42_dispatch = { workspace = true } 10 | frc46_token = { workspace = true } 11 | fvm_actor_utils = { workspace = true } 12 | fvm_ipld_blockstore = { workspace = true } 13 | fvm_ipld_encoding = { workspace = true } 14 | fvm_sdk = { workspace = true } 15 | fvm_shared = { workspace = true } 16 | serde = { workspace = true } 17 | thiserror = { workspace = true } 18 | token_impl = { path = "token_impl" } 19 | 20 | [lib] 21 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 22 | -------------------------------------------------------------------------------- /testing/test_actors/actors/frc46_factory_token/README.md: -------------------------------------------------------------------------------- 1 | # frc46_factory_token 2 | 3 | A configurable native FVM actor that can be used as a factory to implement [FRC-0046](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md) tokens, based on [frc46_token](../frc46_token/README.md) 4 | 5 | Basic configuration is set at construction time as an immutable part of the token state, allowing many tokens to reuse the same actor code. 6 | 7 | This actor also serves as an example of a more complicated token implementation that carries its own state along with the `TokenState` from [frc46_token](../frc46_token/README.md) 8 | 9 | This actor is also used as the token implementation in many of the [integration tests](../testing/fil_token_integration/tests/) 10 | 11 | ## Construction 12 | The `Constructor` method takes the following params struct which configures the new token: 13 | 14 | ```Rust 15 | pub struct ConstructorParams { 16 | pub name: String, 17 | pub symbol: String, 18 | pub granularity: u64, 19 | /// authorised mint operator 20 | /// only this address can mint tokens or remove themselves to permanently disable minting 21 | pub minter: Address, 22 | } 23 | ``` 24 | 25 | These params are set once at construction time and cannot be changed for the life of that token instance, with the exception of being able to clear the `minter` address one time to permanently disable minting. 26 | 27 | No checks or validation are carried out, the onus is on the user to provide appropriate values for their token. 28 | 29 | ## Minting 30 | A basic minting strategy is used, with a single address nominated at construction time as the authorised minter and no limit enforced on the amount they can mint. 31 | 32 | Calls to `Mint` from any other address will abort. 33 | 34 | Minting can be permanently disabled by calling the `DisableMint` method from the authorised minter address. This clears the stored minter address and any further calls to either `Mint` or `DisableMint` will immediately abort. 35 | 36 | 37 | ## token_impl 38 | The core of the factory token implementation lives inside the [token_impl](./token_impl/) crate, so it can be imported without potential conflicts arising from the un-mangled `invoke` method found in the actor code. -------------------------------------------------------------------------------- /testing/test_actors/actors/frc46_factory_token/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::result_large_err)] 2 | 3 | use frc42_dispatch::match_method; 4 | use fvm_actor_utils::{ 5 | blockstore::Blockstore, syscalls::fvm_syscalls::FvmSyscalls, util::ActorRuntime, 6 | }; 7 | use fvm_sdk::NO_DATA_BLOCK_ID; 8 | use fvm_shared::error::ExitCode; 9 | use token_impl::{ 10 | construct_token, deserialize_params, frc46_invoke, return_ipld, FactoryToken, MintParams, 11 | RuntimeError, 12 | }; 13 | 14 | fn token_invoke(method_num: u64, params: u32) -> Result { 15 | let runtime = ActorRuntime::::new_fvm_runtime(); 16 | match_method!(method_num, { 17 | "Constructor" => { 18 | let params = deserialize_params(params); 19 | construct_token(runtime, params) 20 | } 21 | "Mint" => { 22 | let root_cid = runtime.root_cid()?; 23 | let params: MintParams = deserialize_params(params); 24 | let mut token_actor = FactoryToken::load(runtime, &root_cid)?; 25 | let res = token_actor.mint(params)?; 26 | return_ipld(&res) 27 | } 28 | "DisableMint" => { 29 | let root_cid = runtime.root_cid()?; 30 | let mut token_actor = FactoryToken::load(runtime, &root_cid)?; 31 | // disable minting forever 32 | token_actor.disable_mint()?; 33 | // save state 34 | let cid = token_actor.save()?; 35 | token_actor.runtime().set_root(&cid)?; 36 | // no return 37 | Ok(NO_DATA_BLOCK_ID) 38 | } 39 | _ => { 40 | let root_cid = runtime.root_cid()?; 41 | let mut token_actor = FactoryToken::load(runtime, &root_cid)?; 42 | 43 | let res = frc46_invoke(method_num, params, &mut token_actor, |token| { 44 | // `token` is passed through from the original token provided in the function call 45 | // so it won't break mutable borrow rules when used here (trying to use token_actor directly won't work) 46 | let cid = token.save()?; 47 | token.runtime().set_root(&cid)?; 48 | Ok(()) 49 | })?; 50 | match res { 51 | // handled by frc46_invoke, return result 52 | Some(r) => Ok(r), 53 | // method not found 54 | None => { 55 | fvm_sdk::vm::abort( 56 | ExitCode::USR_UNHANDLED_MESSAGE.value(), 57 | Some("Unknown method number"), 58 | ) 59 | } 60 | } 61 | } 62 | }) 63 | } 64 | 65 | #[no_mangle] 66 | pub fn invoke(params: u32) -> u32 { 67 | std::panic::set_hook(Box::new(|info| { 68 | fvm_sdk::vm::abort(ExitCode::USR_ASSERTION_FAILED.value(), Some(&format!("{info}"))) 69 | })); 70 | 71 | let method_num = fvm_sdk::message::method_number(); 72 | match token_invoke(method_num, params) { 73 | Ok(ret) => ret, 74 | Err(err) => fvm_sdk::vm::abort(ExitCode::from(&err).value(), Some(&err.to_string())), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /testing/test_actors/actors/frc46_factory_token/token_impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "token_impl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | cid = { workspace = true } 9 | frc42_dispatch = { workspace = true } 10 | frc46_token = { workspace = true } 11 | fvm_actor_utils = { workspace = true } 12 | fvm_ipld_blockstore = { workspace = true } 13 | fvm_ipld_encoding = { workspace = true } 14 | fvm_sdk = { workspace = true } 15 | fvm_shared = { workspace = true } 16 | multihash-codetable = { workspace = true, features = ["blake2b"] } 17 | serde = { workspace = true } 18 | thiserror = { workspace = true } 19 | 20 | -------------------------------------------------------------------------------- /testing/test_actors/actors/frc46_test_actor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frc46_test_actor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | frc46_token = { workspace = true } 9 | frc42_dispatch = { workspace = true } 10 | frc53_nft = { workspace = true } 11 | fvm_actor_utils = { workspace = true } 12 | 13 | cid = { workspace = true } 14 | fvm_ipld_blockstore = { workspace = true } 15 | fvm_ipld_encoding = { workspace = true } 16 | fvm_sdk = { workspace = true } 17 | fvm_shared = { workspace = true } 18 | serde = { workspace = true } 19 | 20 | [lib] 21 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 22 | -------------------------------------------------------------------------------- /testing/test_actors/actors/frc46_test_actor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::{match_method, method_hash}; 2 | use frc46_token::{ 3 | receiver::{FRC46TokenReceived, FRC46_TOKEN_TYPE}, 4 | token::types::{BurnParams, TransferParams}, 5 | }; 6 | use fvm_actor_utils::receiver::UniversalReceiverParams; 7 | use fvm_ipld_encoding::ipld_block::IpldBlock; 8 | use fvm_ipld_encoding::{de::DeserializeOwned, tuple::*, RawBytes, DAG_CBOR}; 9 | use fvm_sdk as sdk; 10 | use fvm_shared::receipt::Receipt; 11 | use fvm_shared::sys::SendFlags; 12 | use fvm_shared::{address::Address, bigint::Zero, econ::TokenAmount, error::ExitCode}; 13 | use sdk::NO_DATA_BLOCK_ID; 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// Grab the incoming parameters and convert from RawBytes to deserialized struct. 17 | pub fn deserialize_params(params: u32) -> O { 18 | let params = sdk::message::params_raw(params).unwrap().unwrap(); 19 | let params = RawBytes::new(params.data); 20 | params.deserialize().unwrap() 21 | } 22 | 23 | fn return_ipld(value: &T) -> u32 24 | where 25 | T: Serialize + ?Sized, 26 | { 27 | let bytes = fvm_ipld_encoding::to_vec(value).unwrap(); 28 | sdk::ipld::put_block(DAG_CBOR, &bytes).unwrap() 29 | } 30 | 31 | /// Action to take in receiver hook or Action method. 32 | /// 33 | /// This gets serialized and sent along as [`TransferParams::operator_data`]. 34 | #[derive(Serialize, Deserialize, Debug)] 35 | pub enum TestAction { 36 | /// Accept the tokens. 37 | Accept, 38 | /// Reject the tokens (hook aborts). 39 | Reject, 40 | /// Transfer to another address (with operator_data that can provide further instructions). 41 | Transfer(Address, RawBytes), 42 | /// Burn incoming tokens. 43 | Burn, 44 | /// Take action, then abort afterwards. 45 | ActionThenAbort(RawBytes), 46 | /// Transfer to another address (with instructions for recipient), but take alternative action if rejected. 47 | TransferWithFallback { to: Address, instructions: RawBytes, fallback: RawBytes }, 48 | } 49 | 50 | /// Params for Action method call. 51 | /// 52 | /// This gives us a way to supply the token address, since we won't get it as a sender like we do 53 | /// for hook calls. 54 | #[derive(Serialize_tuple, Deserialize_tuple, Debug)] 55 | pub struct ActionParams { 56 | /// Address of the token actor. 57 | pub token_address: Address, 58 | /// Action to take with our token balance. Only [`Transfer`][`TestAction::Transfer`] and 59 | /// [`TestAction::Burn`] actions apply here. 60 | pub action: TestAction, 61 | } 62 | 63 | /// Helper for nesting calls to create action sequences. 64 | /// 65 | /// E.g., transfer and then the receiver hook rejects: 66 | /// 67 | /// ```ignore 68 | /// action(TestAction::Transfer(some_address, action(TestAction::Reject))) 69 | /// ``` 70 | pub fn action(action: TestAction) -> RawBytes { 71 | RawBytes::serialize(action).unwrap() 72 | } 73 | 74 | /// Execute the Transfer action. 75 | fn transfer(token: Address, to: Address, amount: TokenAmount, operator_data: RawBytes) -> Receipt { 76 | let transfer_params = TransferParams { to, amount, operator_data }; 77 | let ret = sdk::send::send( 78 | &token, 79 | method_hash!("Transfer"), 80 | IpldBlock::serialize_cbor(&transfer_params).unwrap(), 81 | TokenAmount::zero(), 82 | None, 83 | SendFlags::empty(), 84 | ) 85 | .unwrap(); 86 | // ignore failures at this level and return the transfer call receipt so caller can decide what to do 87 | Receipt { 88 | exit_code: ret.exit_code, 89 | return_data: ret.return_data.map_or(RawBytes::default(), |b| RawBytes::new(b.data)), 90 | gas_used: 0, 91 | events_root: None, 92 | } 93 | } 94 | 95 | /// Execute the Burn action. 96 | fn burn(token: Address, amount: TokenAmount) -> u32 { 97 | let burn_params = BurnParams { amount }; 98 | let ret = sdk::send::send( 99 | &token, 100 | method_hash!("Burn"), 101 | IpldBlock::serialize_cbor(&burn_params).unwrap(), 102 | TokenAmount::zero(), 103 | None, 104 | SendFlags::empty(), 105 | ) 106 | .unwrap(); 107 | if !ret.exit_code.is_success() { 108 | panic!("burn call failed"); 109 | } 110 | NO_DATA_BLOCK_ID 111 | } 112 | 113 | // handle a TestAction, which could possibly recurse in transfer-with-fallback or action-then-abort cases 114 | fn handle_action(action: TestAction, token_address: Address) -> u32 { 115 | // get our balance 116 | let get_balance = || { 117 | let self_address = Address::new_id(sdk::message::receiver()); 118 | let balance_ret = sdk::send::send( 119 | &token_address, 120 | method_hash!("BalanceOf"), 121 | IpldBlock::serialize_cbor(&self_address).unwrap(), 122 | TokenAmount::zero(), 123 | None, 124 | SendFlags::empty(), 125 | ) 126 | .unwrap(); 127 | if !balance_ret.exit_code.is_success() { 128 | panic!("unable to get balance"); 129 | } 130 | balance_ret.return_data.unwrap().deserialize::().unwrap() 131 | }; 132 | 133 | match action { 134 | TestAction::Accept | TestAction::Reject => { 135 | sdk::vm::abort(ExitCode::USR_ILLEGAL_ARGUMENT.value(), Some("invalid argument")); 136 | } 137 | TestAction::Transfer(to, operator_data) => { 138 | // transfer to a target address 139 | let balance = get_balance(); 140 | let receipt = transfer(token_address, to, balance, operator_data); 141 | return_ipld(&receipt) 142 | } 143 | TestAction::Burn => { 144 | // burn the tokens 145 | let balance = get_balance(); 146 | burn(token_address, balance) 147 | } 148 | TestAction::ActionThenAbort(action) => { 149 | let action: TestAction = action.deserialize().unwrap(); 150 | handle_action(action, token_address); 151 | sdk::vm::abort(ExitCode::USR_UNSPECIFIED.value(), Some("aborted after test action")); 152 | } 153 | TestAction::TransferWithFallback { to, instructions, fallback } => { 154 | let balance = get_balance(); 155 | let receipt = transfer(token_address, to, balance, instructions); 156 | // if transfer failed, try the fallback 157 | if !receipt.exit_code.is_success() { 158 | let fallback_action: TestAction = fallback.deserialize().unwrap(); 159 | handle_action(fallback_action, token_address) 160 | } else { 161 | return_ipld(&receipt) 162 | } 163 | } 164 | } 165 | } 166 | 167 | fn handle_receive_action(action: TestAction, token_address: Address, amount: TokenAmount) -> u32 { 168 | match action { 169 | TestAction::Accept => { 170 | // do nothing, return success 171 | NO_DATA_BLOCK_ID 172 | } 173 | TestAction::Reject => { 174 | // abort to reject transfer 175 | sdk::vm::abort(ExitCode::USR_FORBIDDEN.value(), Some("rejecting transfer")); 176 | } 177 | TestAction::Transfer(to, operator_data) => { 178 | // transfer to a target address 179 | let receipt = transfer(token_address, to, amount, operator_data); 180 | return_ipld(&receipt) 181 | } 182 | TestAction::Burn => { 183 | // burn the tokens 184 | burn(Address::new_id(sdk::message::caller()), amount) 185 | } 186 | TestAction::ActionThenAbort(action) => { 187 | let action: TestAction = action.deserialize().unwrap(); 188 | handle_receive_action(action, token_address, amount); 189 | sdk::vm::abort(ExitCode::USR_UNSPECIFIED.value(), Some("aborted after test action")); 190 | } 191 | TestAction::TransferWithFallback { to, instructions, fallback } => { 192 | let receipt = transfer(token_address, to, amount.clone(), instructions); 193 | // if transfer failed, try the fallback 194 | if !receipt.exit_code.is_success() { 195 | let fallback_action: TestAction = fallback.deserialize().unwrap(); 196 | handle_receive_action(fallback_action, token_address, amount) 197 | } else { 198 | return_ipld(&receipt) 199 | } 200 | } 201 | } 202 | } 203 | 204 | #[no_mangle] 205 | fn invoke(input: u32) -> u32 { 206 | std::panic::set_hook(Box::new(|info| { 207 | sdk::vm::abort(ExitCode::USR_ASSERTION_FAILED.value(), Some(&format!("{info}"))) 208 | })); 209 | 210 | let method_num = sdk::message::method_number(); 211 | match_method!(method_num, { 212 | "Constructor" => { 213 | NO_DATA_BLOCK_ID 214 | }, 215 | "Receive" => { 216 | // Received is passed a UniversalReceiverParams 217 | let params: UniversalReceiverParams = deserialize_params(input); 218 | 219 | // reject if not an FRC46 token 220 | // we don't know how to inspect other payloads here 221 | if params.type_ != FRC46_TOKEN_TYPE { 222 | panic!("invalid token type, rejecting transfer"); 223 | } 224 | 225 | // get token transfer data 226 | let token_params: FRC46TokenReceived = params.payload.deserialize().unwrap(); 227 | 228 | // todo: examine the operator_data to determine our next move 229 | let action: TestAction = token_params.operator_data.deserialize().unwrap(); 230 | handle_receive_action(action, Address::new_id(sdk::message::caller()), token_params.amount) 231 | }, 232 | "Action" => { 233 | // take action independent of the receiver hook 234 | let params: ActionParams = deserialize_params(input); 235 | 236 | handle_action(params.action, params.token_address) 237 | } 238 | _ => { 239 | sdk::vm::abort( 240 | ExitCode::USR_UNHANDLED_MESSAGE.value(), 241 | Some("Unknown method number"), 242 | ); 243 | } 244 | }) 245 | } 246 | -------------------------------------------------------------------------------- /testing/test_actors/actors/frc53_test_actor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frc53_test_actor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | frc46_token = { workspace = true } 9 | fvm_actor_utils = { workspace = true } 10 | frc42_dispatch = { workspace = true } 11 | frc53_nft = { workspace = true } 12 | 13 | cid = { workspace = true } 14 | fvm_ipld_blockstore = { workspace = true } 15 | fvm_ipld_encoding = { workspace = true } 16 | fvm_sdk = { workspace = true } 17 | fvm_shared = { workspace = true } 18 | serde = { workspace = true } 19 | 20 | [lib] 21 | crate-type = ["cdylib"] ## cdylib is necessary for Wasm build 22 | 23 | -------------------------------------------------------------------------------- /testing/test_actors/actors/frc53_test_actor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use frc42_dispatch::{match_method, method_hash}; 2 | use frc53_nft::receiver::FRC53TokenReceived; 3 | use frc53_nft::receiver::FRC53_TOKEN_TYPE; 4 | use frc53_nft::types::TokenID; 5 | use frc53_nft::types::TransferParams; 6 | use fvm_actor_utils::receiver::UniversalReceiverParams; 7 | use fvm_ipld_encoding::ipld_block::IpldBlock; 8 | use fvm_ipld_encoding::{de::DeserializeOwned, tuple::*, RawBytes, DAG_CBOR}; 9 | use fvm_sdk as sdk; 10 | use fvm_shared::receipt::Receipt; 11 | use fvm_shared::sys::SendFlags; 12 | use fvm_shared::{address::Address, bigint::Zero, econ::TokenAmount, error::ExitCode}; 13 | use sdk::NO_DATA_BLOCK_ID; 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// Grab the incoming parameters and convert from [`RawBytes`] to deserialized struct. 17 | pub fn deserialize_params(params: u32) -> O { 18 | let params = sdk::message::params_raw(params).unwrap().unwrap(); 19 | let params = RawBytes::new(params.data); 20 | params.deserialize().unwrap() 21 | } 22 | 23 | fn return_ipld(value: &T) -> u32 24 | where 25 | T: Serialize + ?Sized, 26 | { 27 | let bytes = fvm_ipld_encoding::to_vec(value).unwrap(); 28 | sdk::ipld::put_block(DAG_CBOR, &bytes).unwrap() 29 | } 30 | 31 | /// Action to take in receiver hook or Action method. 32 | /// 33 | /// This gets serialized and sent along as operator_data. 34 | #[derive(Serialize, Deserialize, Debug)] 35 | pub enum TestAction { 36 | /// Accept the tokens. 37 | Accept, 38 | /// Reject the tokens (hook aborts). 39 | Reject, 40 | /// Transfer to another address (with operator_data that can provide further instructions). 41 | Transfer(Address, Vec, RawBytes), 42 | /// Burn incoming tokens. 43 | Burn(Vec), 44 | } 45 | 46 | /// Params for Action method call. 47 | /// 48 | /// This gives us a way to supply the token address, since we won't get it as a sender like we do 49 | /// for hook calls. 50 | #[derive(Serialize_tuple, Deserialize_tuple, Debug)] 51 | pub struct ActionParams { 52 | /// Address of the token actor. 53 | pub token_address: Address, 54 | /// Action to take with our token balance. Only Transfer and Burn actions apply here. 55 | pub action: TestAction, 56 | } 57 | 58 | /// Helper for nesting calls to create action sequences. 59 | /// 60 | /// E.g., transfer and then the receiver hook rejects: 61 | /// 62 | /// ```ignore 63 | /// action(TestAction::Transfer(some_address, action(TestAction::Reject))); 64 | /// ``` 65 | pub fn action(action: TestAction) -> RawBytes { 66 | RawBytes::serialize(action).unwrap() 67 | } 68 | 69 | /// Execute the Transfer action. 70 | fn transfer(token: Address, to: Address, token_ids: Vec, operator_data: RawBytes) -> u32 { 71 | let transfer_params = TransferParams { to, token_ids, operator_data }; 72 | let ret = sdk::send::send( 73 | &token, 74 | method_hash!("Transfer"), 75 | IpldBlock::serialize_cbor(&transfer_params).unwrap(), 76 | TokenAmount::zero(), 77 | None, 78 | SendFlags::empty(), 79 | ) 80 | .unwrap(); 81 | // ignore failures at this level and return the transfer call receipt so caller can decide what to do 82 | return_ipld(&Receipt { 83 | exit_code: ret.exit_code, 84 | return_data: ret.return_data.map_or(RawBytes::default(), |b| RawBytes::new(b.data)), 85 | gas_used: 0, 86 | events_root: None, 87 | }) 88 | } 89 | 90 | /// Execute the Burn action. 91 | fn burn(token: Address, token_ids: Vec) -> u32 { 92 | let ret = sdk::send::send( 93 | &token, 94 | method_hash!("Burn"), 95 | IpldBlock::serialize_cbor(&token_ids).unwrap(), 96 | TokenAmount::zero(), 97 | None, 98 | SendFlags::empty(), 99 | ) 100 | .unwrap(); 101 | if !ret.exit_code.is_success() { 102 | panic!("burn call failed"); 103 | } 104 | NO_DATA_BLOCK_ID 105 | } 106 | 107 | #[no_mangle] 108 | fn invoke(input: u32) -> u32 { 109 | std::panic::set_hook(Box::new(|info| { 110 | sdk::vm::abort(ExitCode::USR_ASSERTION_FAILED.value(), Some(&format!("{info}"))) 111 | })); 112 | 113 | let method_num = sdk::message::method_number(); 114 | match_method!(method_num, { 115 | "Constructor" => { 116 | NO_DATA_BLOCK_ID 117 | }, 118 | "Receive" => { 119 | // Received is passed a UniversalReceiverParams 120 | let params: UniversalReceiverParams = deserialize_params(input); 121 | 122 | // reject if not a set of FRC53 NFTs 123 | // we don't know how to inspect other payloads here 124 | if params.type_ != FRC53_TOKEN_TYPE { 125 | panic!("invalid token type, rejecting transfer"); 126 | } 127 | 128 | // get token transfer data 129 | let token_params: FRC53TokenReceived = params.payload.deserialize().unwrap(); 130 | 131 | // todo: examine the operator_data to determine our next move 132 | let action: TestAction = token_params.operator_data.deserialize().unwrap(); 133 | match action { 134 | TestAction::Accept => { 135 | // do nothing, return success 136 | NO_DATA_BLOCK_ID 137 | } 138 | TestAction::Reject => { 139 | // abort to reject transfer 140 | sdk::vm::abort( 141 | ExitCode::USR_FORBIDDEN.value(), 142 | Some("rejecting transfer"), 143 | ); 144 | } 145 | TestAction::Transfer(to, token_ids, operator_data) => { 146 | // transfer to a target address 147 | transfer(Address::new_id(sdk::message::caller()), to, token_ids, operator_data) 148 | } 149 | TestAction::Burn(token_ids) => { 150 | // burn the tokens 151 | burn(Address::new_id(sdk::message::caller()), token_ids) 152 | } 153 | } 154 | }, 155 | "Action" => { 156 | // take action independent of the receiver hook 157 | let params: ActionParams = deserialize_params(input); 158 | 159 | match params.action { 160 | TestAction::Accept | TestAction::Reject => { 161 | sdk::vm::abort( 162 | ExitCode::USR_ILLEGAL_ARGUMENT.value(), 163 | Some("invalid argument"), 164 | ); 165 | } 166 | TestAction::Transfer(to, token_ids, operator_data) => { 167 | // transfer to a target address 168 | transfer(params.token_address, to, token_ids, operator_data) 169 | } 170 | TestAction::Burn(token_ids) => { 171 | // burn the tokens 172 | burn(params.token_address, token_ids) 173 | } 174 | } 175 | } 176 | _ => { 177 | sdk::vm::abort( 178 | ExitCode::USR_UNHANDLED_MESSAGE.value(), 179 | Some("Unknown method number"), 180 | ); 181 | } 182 | }) 183 | } 184 | -------------------------------------------------------------------------------- /testing/test_actors/build.rs: -------------------------------------------------------------------------------- 1 | //! Follows pattern for building wasm actors established in ref-fvm 2 | //! https://github.com/filecoin-project/ref-fvm/blob/master/testing/test_actors/build.rs 3 | use std::error::Error; 4 | use std::io::{BufRead, BufReader}; 5 | use std::path::Path; 6 | use std::process::{Command, Stdio}; 7 | use std::thread; 8 | 9 | const ACTORS: &[&str] = &[ 10 | "basic_nft_actor", 11 | "basic_receiving_actor", 12 | "basic_token_actor", 13 | "basic_transfer_actor", 14 | "frc46_test_actor", 15 | "frc53_test_actor", 16 | "greeter", 17 | "frc46_factory_token", 18 | ]; 19 | 20 | fn main() -> Result<(), Box> { 21 | // Cargo executable location. 22 | let cargo = std::env::var_os("CARGO").expect("no CARGO env var"); 23 | 24 | let out_dir = std::env::var_os("OUT_DIR") 25 | .as_ref() 26 | .map(Path::new) 27 | .map(|p| p.join("bundle")) 28 | .expect("no OUT_DIR env var"); 29 | println!("cargo:warning=out_dir: {:?}", &out_dir); 30 | 31 | let manifest_path = 32 | Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR unset")) 33 | .join("Cargo.toml"); 34 | 35 | for file in ["Cargo.toml", "src", "actors"] { 36 | println!("cargo:rerun-if-changed={}", file); 37 | } 38 | 39 | // Cargo build command for all actors at once. 40 | let mut cmd = Command::new(cargo); 41 | cmd.arg("build") 42 | .args(ACTORS.iter().map(|pkg| "-p=".to_owned() + pkg)) 43 | .arg("--target=wasm32-unknown-unknown") 44 | .arg("--profile=wasm") 45 | .arg("--locked") 46 | .arg("--manifest-path=".to_owned() + manifest_path.to_str().unwrap()) 47 | .stdout(Stdio::piped()) 48 | .stderr(Stdio::piped()) 49 | // We are supposed to only generate artifacts under OUT_DIR, 50 | // so set OUT_DIR as the target directory for this build. 51 | .env("CARGO_TARGET_DIR", &out_dir) 52 | // As we are being called inside a build-script, this env variable is set. However, we set 53 | // our own `RUSTFLAGS` and thus, we need to remove this. Otherwise cargo favors this 54 | // env variable. 55 | .env_remove("CARGO_ENCODED_RUSTFLAGS"); 56 | 57 | // Print out the command line we're about to run. 58 | println!("cargo:warning=cmd={:?}", &cmd); 59 | 60 | // Launch the command. 61 | let mut child = cmd.spawn().expect("failed to launch cargo build"); 62 | 63 | // Pipe the output as cargo warnings. Unfortunately this is the only way to 64 | // get cargo build to print the output. 65 | let stdout = child.stdout.take().expect("no stdout"); 66 | let stderr = child.stderr.take().expect("no stderr"); 67 | let j1 = thread::spawn(move || { 68 | for line in BufReader::new(stderr).lines() { 69 | println!("cargo:warning={:?}", line.unwrap()); 70 | } 71 | }); 72 | let j2 = thread::spawn(move || { 73 | for line in BufReader::new(stdout).lines() { 74 | println!("cargo:warning={:?}", line.unwrap()); 75 | } 76 | }); 77 | 78 | j1.join().unwrap(); 79 | j2.join().unwrap(); 80 | 81 | let result = child.wait().expect("failed to wait for build to finish"); 82 | if !result.success() { 83 | return Err("actor build failed".into()); 84 | } 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /testing/test_actors/src/lib.rs: -------------------------------------------------------------------------------- 1 | // constants for wasm build artifacts 2 | #![allow(dead_code)] 3 | macro_rules! wasm_bin { 4 | ($x: expr) => { 5 | concat!(env!("OUT_DIR"), "/bundle/wasm32-unknown-unknown/wasm/", $x, ".wasm") 6 | }; 7 | } 8 | 9 | pub const BASIC_NFT_ACTOR_BINARY: &[u8] = include_bytes!(wasm_bin!("basic_nft_actor")); 10 | pub const BASIC_RECEIVING_ACTOR_BINARY: &[u8] = include_bytes!(wasm_bin!("basic_receiving_actor")); 11 | pub const BASIC_TOKEN_ACTOR_BINARY: &[u8] = include_bytes!(wasm_bin!("basic_token_actor")); 12 | pub const BASIC_TRANSFER_ACTOR_BINARY: &[u8] = include_bytes!(wasm_bin!("basic_transfer_actor")); 13 | pub const FRC46_TEST_ACTOR_BINARY: &[u8] = include_bytes!(wasm_bin!("frc46_test_actor")); 14 | pub const FRC53_TEST_ACTOR_BINARY: &[u8] = include_bytes!(wasm_bin!("frc53_test_actor")); 15 | pub const FRC46_FACTORY_TOKEN_ACTOR_BINARY: &[u8] = 16 | include_bytes!(wasm_bin!("frc46_factory_token")); 17 | --------------------------------------------------------------------------------