├── .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 | [](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