abigen
abigen!
is a procedural macro -- it generates code. It accepts inputs in the format of:
ProgramType(name="MyProgramType", abi="my_program-abi.json")...
where:
-
ProgramType
is one of:Contract
,Script
orPredicate
, -
name
is the name that will be given to the generated bindings, -
abi
is either a path to the JSON ABI file or its actual contents.
So, an abigen!
which generates bindings for two contracts and one script looks like this:
abigen!(
Contract(
name = "ContractA",
abi = "e2e/sway/bindings/sharing_types/contract_a/out/release/contract_a-abi.json"
),
Contract(
name = "ContractB",
abi = "e2e/sway/bindings/sharing_types/contract_b/out/release/contract_b-abi.json"
),
Script(
name = "MyScript",
abi = "e2e/sway/scripts/arguments/out/release/arguments-abi.json"
),
Predicate(
name = "MyPredicateEncoder",
abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
),
);
How does the generated code look?
A rough overview:
pub mod abigen_bindings {
pub mod contract_a_mod {
struct SomeCustomStruct{/*...*/};
// other custom types used in the contract
struct ContractA {/*...*/};
impl ContractA {/*...*/};
// ...
}
pub mod contract_b_mod {
// ...
}
pub mod my_script_mod {
// ...
}
pub mod my_predicate_mod{
// ...
}
pub mod shared_types{
// ...
}
}
pub use contract_a_mod::{/*..*/};
pub use contract_b_mod::{/*..*/};
pub use my_predicate_mod::{/*..*/};
pub use shared_types::{/*..*/};
Each ProgramType
gets its own mod
based on the name
given in the abigen!
. Inside the respective mods, the custom types used by that program are generated, and the bindings through which the actual calls can be made.
One extra mod
called shared_types
is generated if abigen!
detects that the given programs share types. Instead of each mod
regenerating the type for itself, the type is lifted out into the shared_types
module, generated only once, and then shared between all program bindings that use it. Reexports are added to each mod so that even if a type is deemed shared, you can still access it as though each mod
had generated the type for itself (i.e. my_contract_mod::SharedType
).
A type is deemed shared if its name and definition match up. This can happen either because you've used the same library (a custom one or a type from the stdlib
) or because you've happened to define the exact same type.
Finally, pub use
statements are inserted, so you don't have to fully qualify the generated types. To avoid conflict, only types that have unique names will get a pub use
statement. If you find rustc
can't find your type, it might just be that there is another generated type with the same name. To fix the issue just qualify the path by doing abigen_bindings::whatever_contract_mod::TheType
.
Note: It is highly encouraged that you generate all your bindings in one
abigen!
call. Doing it in this manner will allow type sharing and avoid name collisions you'd normally get when callingabigen!
multiple times inside the same namespace. If you choose to proceed otherwise, keep in mind the generated code overview presented above and appropriately separate theabigen!
calls into different modules to resolve the collision.
Type paths
Normally when using types from libraries in your contract, script or predicate, they'll be generated directly under the main mod
of your program bindings, i.e. a type in a contract binding MyContract
imported from a library some_library
would be generated under abigen_bindings::my_contract_mod::SomeLibraryType
.
This can cause problems if you happen to have two types with the same name in different libraries of your program.
This behavior can be changed to include the library path by compiling your Sway project with the following:
forc build --json-abi-with-callpaths
Now the type from the previous example will be generated under abigen_bindings::my_contract_mod::some_library::SomeLibraryType
.
This might only become relevant if your type isn't reexported. This can happen, as explained previously, if your type does not have a unique name across all bindings inside one abigen!
call. You'll then need to fully qualify the access to it.
Including type paths will eventually become the default and the flag will be removed.
Using the bindings
Let's look at a contract with two methods: initialize_counter(arg: u64) -> u64
and increment_counter(arg: u64) -> u64
, with the following JSON ABI:
{
"types": [
{
"typeId": 0,
"type": "u64",
"components": null,
"typeParameters": null
}
],
"functions": [
{
"inputs": [
{
"name": "value",
"type": 0,
"typeArguments": null
}
],
"name": "initialize_counter",
"output": {
"name": "",
"type": 0,
"typeArguments": null
}
},
{
"inputs": [
{
"name": "value",
"type": 0,
"typeArguments": null
}
],
"name": "increment_counter",
"output": {
"name": "",
"type": 0,
"typeArguments": null
}
}
]
}
By doing this:
use fuels::prelude::*;
// Replace with your own JSON abi path (relative to the root of your crate)
abigen!(Contract(
name = "MyContractName",
abi = "examples/rust_bindings/src/abi.json"
));
or this:
use fuels::prelude::*;
abigen!(Contract(
name = "MyContract",
abi = r#"
{
"types": [
{
"typeId": 0,
"type": "u64",
"components": null,
"typeParameters": null
}
],
"functions": [
{
"inputs": [
{
"name": "value",
"type": 0,
"typeArguments": null
}
],
"name": "initialize_counter",
"output": {
"name": "",
"type": 0,
"typeArguments": null
}
},
{
"inputs": [
{
"name": "value",
"type": 0,
"typeArguments": null
}
],
"name": "increment_counter",
"output": {
"name": "",
"type": 0,
"typeArguments": null
}
}
]
}
"#
));
you'll generate this (shortened for brevity's sake):
pub mod abigen_bindings {
pub mod my_contract_mod {
use ::fuels::{
accounts::{Account, ViewOnlyAccount},
core::{
codec,
traits::{Parameterize, Tokenizable},
Configurables,
},
programs::{
contract::{self, ContractCallHandler},
logs::{self, LogDecoder},
},
types::{bech32::Bech32ContractId, errors::Result, AssetId},
};
pub struct MyContract<T: Account> {
contract_id: Bech32ContractId,
account: T,
log_decoder: LogDecoder,
}
impl<T: Account> MyContract<T> {
pub fn new(
contract_id: impl ::core::convert::Into<Bech32ContractId>,
account: T,
) -> Self {
let contract_id: Bech32ContractId = contract_id.into();
let log_decoder = LogDecoder::new(logs::log_formatters_lookup(
vec![],
contract_id.clone().into(),
));
Self {
contract_id,
account,
log_decoder,
}
}
pub fn contract_id(&self) -> &Bech32ContractId {
&self.contract_id
}
pub fn account(&self) -> T {
self.account.clone()
}
pub fn with_account<U: Account>(&self, account: U) -> MyContract<U> {
MyContract {
contract_id: self.contract_id.clone(),
account,
log_decoder: self.log_decoder.clone(),
}
}
pub async fn get_balances(&self) -> Result<::std::collections::HashMap<AssetId, u64>> {
ViewOnlyAccount::try_provider(&self.account)?
.get_contract_balances(&self.contract_id)
.await
.map_err(::std::convert::Into::into)
}
pub fn methods(&self) -> MyContractMethods<T> {
MyContractMethods {
contract_id: self.contract_id.clone(),
account: self.account.clone(),
log_decoder: self.log_decoder.clone(),
}
}
}
pub struct MyContractMethods<T: Account> {
contract_id: Bech32ContractId,
account: T,
log_decoder: LogDecoder,
}
impl<T: Account> MyContractMethods<T> {
#[doc = "Calls the contract's `initialize_counter` function"]
pub fn initialize_counter(&self, value: u64) -> ContractCallHandler<T, u64> {
contract::method_hash(
self.contract_id.clone(),
self.account.clone(),
codec::encode_fn_selector("initialize_counter"),
&[Tokenizable::into_token(value)],
self.log_decoder.clone(),
false,
ABIEncoder::new(EncoderConfig::default()),
)
}
#[doc = "Calls the contract's `increment_counter` function"]
pub fn increment_counter(&self, value: u64) -> ContractCallHandler<T, u64> {
contract::method_hash(
self.contract_id.clone(),
self.account.clone(),
codec::encode_fn_selector("increment_counter"),
&[value.into_token()],
self.log_decoder.clone(),
false,
ABIEncoder::new(EncoderConfig::default()),
)
}
}
impl<T: Account> contract::SettableContract for MyContract<T> {
fn id(&self) -> Bech32ContractId {
self.contract_id.clone()
}
fn log_decoder(&self) -> LogDecoder {
self.log_decoder.clone()
}
}
#[derive(Clone, Debug, Default)]
pub struct MyContractConfigurables {
offsets_with_data: ::std::vec::Vec<(u64, ::std::vec::Vec<u8>)>,
}
impl MyContractConfigurables {
pub fn new() -> Self {
::std::default::Default::default()
}
}
impl From<MyContractConfigurables> for Configurables {
fn from(config: MyContractConfigurables) -> Self {
Configurables::new(config.offsets_with_data)
}
}
}
}
pub use abigen_bindings::my_contract_mod::MyContract;
pub use abigen_bindings::my_contract_mod::MyContractConfigurables;
pub use abigen_bindings::my_contract_mod::MyContractMethods;
Note: that is all generated code. No need to write any of that. Ever. The generated code might look different from one version to another, this is just an example to give you an idea of what it looks like.
Then, you're able to use it to call the actual methods on the deployed contract:
// This will generate your contract's methods onto `MyContract`.
// This means an instance of `MyContract` will have access to all
// your contract's methods that are running on-chain!
abigen!(Contract(
name = "MyContract",
abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
));
// This is an instance of your contract which you can use to make calls to your functions
let contract_instance = MyContract::new(contract_id_2, wallet);
let response = contract_instance
.methods()
.initialize_counter(42) // Build the ABI call
.call() // Perform the network call
.await?;
assert_eq!(42, response.value);
let response = contract_instance
.methods()
.increment_counter(10)
.call()
.await?;
assert_eq!(52, response.value);