Recall Contracts
Recall core Solidity contracts and libraries
Table of Contents
Background
This project is built with Foundry and contains the core contracts
for Recall. It includes the following:
Recall.sol: An ERC20 token implementation.Faucet.sol: The accompanying onchain faucet (rate limiter) contract for dripping testnet funds.CreditManager.sol: Manage subnet credit, including credit purchases, approvals/rejections, and
related read-only operations (uses theLibCreditandLibWasmlibraries).BucketManager.sol: Manage buckets, including creating buckets, listing buckets, querying
objects, and other object-related operations (uses theLibBucketandLibWasmlibraries).ValidatorGater.sol: A contract for managing validator access.interfaces/ICredit.sol: The interface for the credit contract.types/: Various method parameters and return types for core contracts.utils/LibCredit.sol: A library for interacting with credits, wrapped by theCreditcontract.utils/LibBucket.sol: A library for interacting with buckets, wrapped by theBucketManager
contract.utils/LibWasm.sol: A library for facilitating proxy calls to WASM contracts from Solidity.utils/solidity-cbor: Libraries for encoding and decoding CBOR data, used in proxy WASM calls
(forked from this repo).utils/Base32.sol: Utilities for encoding and decoding base32.utils/Blake2b.sol: Utilities for Blake2b hashing (used for WASMt2addresses).
Deployments
The following contracts are deployed in the testnet environment (Filecoin Calibration or the Recall
subnet):
| Contract | Chain | Address |
|---|---|---|
| Recall (ERC20) | Calibration | 0x20d8a696091153c4d4816ba1fdefe113f71e0905 |
| Faucet | Subnet | 0x7Aff9112A46D98A455f4d4F93c0e3D2438716A44 |
| BlobManager | Subnet | 0xTODO |
| BucketManager | Subnet | 0xTODO |
| CreditManager | Subnet | 0xTODO |
| ValidatorGater | Subnet | 0x880126f3134EdFBa4f1a65827D5870f021bb7124 |
To get testnet tokens, visit:
https://faucet.node-0.testnet.recall.network. Also,
you can check out the foundry.toml file to see the RPC URLs for each network (described in more
detail below).
Usage
Setup
First, clone the repo, and be sure foundry is installed on your machine (see
here):
git clone https://github.com/recallnet/contracts.git
cd contractsInstall the dependencies and build the contracts, which will output the ABIs to the out/
directory:
pnpm install
forge install
forge buildAlso, you can clean the build artifacts with:
pnpm cleanDeploying contracts
The scripts for deploying contracts are in script/ directory:
Recall.s.sol: Deploy the Recall ERC20 contract.Faucet.s.sol: Deploy the faucet contract.ValidatorGater.s.sol: Deploy the validator gater contract.CreditManager.s.sol: Deploy the credit contract.BlobManager.s.sol: Deploy the blobs contract.BucketManager.s.sol: Deploy the Bucket Manager contract.Bridge.s.sol: Deploy the bridge contract—relevant for the Recall ERC20 on live chains.
[!NOTE] If you're deploying to the Recall subnet or Filecoin Calibration, you'll need to
(significantly) bump the gas estimation multiplier by adding a-g 100000flag to the
forge scriptcommand.
Each script uses the --private-key flag, and the script examples below demonstrate its usage with
a PRIVATE_KEY (hex prefixed) environment variable for the account that deploy the contract:
export PRIVATE_KEY=<0x...>You can run a script with forge script, passing the script path/name and any arguments. The
--rpc-url flag can make use of the RPC endpoints defined in foundry.toml:
localnet_parent: Deploy to localnet and the parent (Anvil) node.localnet_subnet: Deploy to localnet and the subnet node.testnet_parent: Deploy to testnet and the parent network (Filecoin Calibration).testnet_subnet: Deploy to testnet and the subnet (Recall).devnet: Deploy to the devnet network.
The --target-contract (--tc) should be DeployScript, and it takes an argument for the
environment used by the --sig flag below:
local: Local chain(for either localnet or devnet)testnet: Testnet chainethereumorfilecoin: Mainnet chain (note: mainnet is not available yet)
Most scripts use the --sig flag with run(string) (or run(string,uint256) for the faucet) to
execute deployment with the given argument above, and the --broadcast flag actually sends the
transaction to the network. Recall that you must set -g 100000 to ensure the gas estimate is
sufficiently high.
Mainnet deployments require the address of the Axelar Interchain Token Service on chain you are
deploying to, which is handled in the ERC20's DeployScript logic.
Localnet
Recall ERC20
Deploy the Recall ERC20 contract to the localnet parent chain (i.e., http://localhost:8545). Note
the -g flag is not used here since the gas estimate is sufficiently low on Anvil.
forge script script/Recall.s.sol --tc DeployScript --sig 'run(string)' local --rpc-url localnet_parent --private-key $PRIVATE_KEY --broadcast -vvFaucet
Deploy the Faucet contract to the localnet subnet. The second argument is the initial supply of
Recall tokens, owned by the deployer's account which will be transferred to the faucet contract
(e.g., 5000 with 10**18 decimal units).
PRIVATE_KEY=<0x...> forge script script/Faucet.s.sol --tc DeployScript --sig 'run(uint256)' 5000000000000000000000 --rpc-url localnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvCredit
Deploy the Credit contract to the localnet subnet:
forge script script/CreditManager.s.sol --tc DeployScript --sig 'run()' --rpc-url localnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvBuckets
Deploy the Bucket Manager contract to the localnet subnet:
forge script script/BucketManager.s.sol --tc DeployScript --sig 'run()' --rpc-url localnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvBlobs
Deploy the Blob Manager contract to the localnet subnet:
forge script script/BlobManager.s.sol --tc DeployScript --sig 'run()' --rpc-url localnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvTestnet
Recall ERC20
Deploy the Recall ERC20 contract to the testnet parent chain. Note the -g flag is used here
(this differs from the localnet setup above since we're deploying to Filecoin Calibration);
forge script script/Recall.s.sol --tc DeployScript --sig 'run(string)' testnet --rpc-url testnet_parent --private-key $PRIVATE_KEY --broadcast -g 100000 -vvFaucet
Deploy the Faucet contract to the testnet subnet. The second argument is the initial supply of
Recall tokens, owned by the deployer's account which will be transferred to the faucet contract
(e.g., 5000 with 10**18 decimal units).
forge script script/Faucet.s.sol --tc DeployScript --sig 'run(uint256)' 5000000000000000000000 --rpc-url testnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvCredit
Deploy the Credit Manager contract to the testnet subnet:
forge script script/CreditManager.s.sol --tc DeployScript --sig 'run()' --rpc-url testnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvBuckets
Deploy the Bucket Manager contract to the testnet subnet:
forge script script/BucketManager.s.sol --tc DeployScript --sig 'run()' --rpc-url testnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvBlobs
Deploy the Blob Manager contract to the testnet subnet:
forge script script/BlobManager.s.sol --tc DeployScript --sig 'run()' --rpc-url testnet_subnet --private-key $PRIVATE_KEY --broadcast -g 100000 -vvDevnet
The devnet does not have the concept of a "parent" chain, so all RPCs would use --rpc-url devnet
and follow the same pattern as the deployments above.
If you're trying to simply deploy to an Anvil node (i.e., http://localhost:8545), you can use the
same pattern, or just explicitly set the RPC URL:
forge script script/Recall.s.sol --tc DeployScript --sig 'run(string)' local --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY --broadcast -vvMainnet
Mainnet is not yet available. The RPC URLs (mainnet_parent and mainnet_subnet) are placeholders,
pointing to the same environment as the testnet.
Recall ERC20
However, if you'd like to deploy the RECALL ERC20 contract to mainnet Ethereum or Filecoin, you can
use the following. Note these will enable behavior for the Axelar Interchain Token Service.
Deploy to Ethereum:
forge script script/Recall.s.sol:DeployScript --sig 'run(string)' ethereum --rpc-url https://eth.merkle.io --private-key $PRIVATE_KEY --broadcast -vvAnd for Filecoin:
forge script script/Recall.s.sol:DeployScript --sig 'run(string)' filecoin --rpc-url https://api.node.glif.io/rpc/v1 --private-key $PRIVATE_KEY --broadcast -vvDevelopment
Scripts
Deployment scripts are described above. Additional pnpm scripts are available in package.json:
format: Runforge fmtto format the code.lint: Runforge fmtandsolhintto check for linting errors.test: Runforge testto run the tests.clean: Runforge cleanto clean the build artifacts.
Examples usage
Below are examples for interacting with various contracts. We'll set a few environment variables to
demonstrate usage patterns, all of which will assume you've first defined the following. We'll first
set the PRIVATE_KEY to the hex-encoded private key (same as with the deployment scripts above),
and an arbitrary EVM_ADDRESS that is the public key of this private key.
export PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
export EVM_ADDRESS=0x90f79bf6eb2c4f870365e785982e1f101e93b906The --rpc-url flag can use the same RPC URLs as the deployments scripts, as defined in
foundry.toml. For simplicity sake, we'll define an environment variable ETH_RPC_URL set to one
of these RPC URLs so these examples can be run as-is:
export ETH_RPC_URL=testnet_subnetThe subsequent sections will define other environment variables as needed.
Credit contract
You can interact with the existing credit contract on the testnet via the address above. If you're
working on localnet, you'll have to deploy this yourself. Here's a quick one-liner to do so—also
setting the CREDIT environment variable to the deployed address:
CREDIT=$(forge script script/CreditManager.s.sol \
--tc DeployScript \
--sig 'run()' \
--rpc-url localnet_subnet \
--private-key $PRIVATE_KEY \
--broadcast \
-g 100000 \
| grep "0: contract CreditManager" | awk '{print $NF}')
Methods
The following methods are available on the credit contract, shown with their function signatures.
Note that overloads are available for some methods, primarily, where the underlying WASM contract
accepts "optional" arguments. All of the method parameters and return types can be found in
util/CreditTypes.sol.
getAccount(address): Get credit account info for an address.getCreditStats(): Get credit stats.getCreditApproval(address,address): Get credit approvalfromone accounttoanother, if it
exists.getCreditBalance(address): Get credit balance for an address.buyCredit(): Buy credit for themsg.sender.buyCredit(address): Buy credit for the given address.approveCredit(address): Approve credit for an address (to), assumingmsg.senderis the owner
of the credit (inferred asfromin underlying logic).approveCredit(address,address): Approve credit for the credit owner (from) for an address
(to). Effectively, the same asapproveCredit(address)but explicitly sets thefromaddress.approveCredit(address,address,address[]): Approve credit for the credit owner (from) for an
address (to), with a restriction on the caller address (caller) (e.g., enforcetocan only
use credit atcaller).approveCredit(address,address,address[],uint256,uint256,uint64): Approve credit for the credit
owner (from) for an address (to), providing all of the optional fields (caller,
creditLimit,gasFeeLimit, andttl).setCreditSponsor(address,address): Set the credit sponsor for an address (from) to an address
(sponsor, use zero address if unused).revokeCredit(address): Revoke credit for an address (to), assumingmsg.senderis the owner
of the credit (inferred asfromin underlying logic).revokeCredit(address,address): Revoke credit for the credit owner (from) for an address
(to). Effectively, the same asapproveCredit(address)but explicitly sets thefromaddress.revokeCredit(address,address,address): Revoke credit for the credit owner (from) for an
address (to), with a restriction on the caller address (caller) (e.g., remove permissions for
toatcaller).
The overloads above have a bit of nuance when it comes to "optional" arguments. See the ICredit
interface in interfaces/ICredit.sol for more details. For example, zero values are interpreted as
null values when encoded in WASM calls.
Examples
Make sure you've already set the PRIVATE_KEY, EVM_ADDRESS, and ETH_RPC_URL environment
variables. Then, define a CREDIT environment variable, which points to the credit contract
deployment address. For example:
export CREDIT=0xAfC2973fbc4213DA7007A6b9459003A89c9C5b0EAnd lastly, we'll define a RECEIVER_ADDR environment variable, which points to the to address
we'll be approving and revoking credit for. For example:
export RECEIVER_ADDR=0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65Get account info
We can get the credit account info for the address at EVM_ADDRESS (the variable we set above), or
you could provide any account's EVM public key that exists in the subnet.
cast abi-decode "getAccount(address)((uint64,uint256,uint256,address,uint64,(address,(uint256,uint256,uint64,uint256,uint256))[],(address,(uint256,uint256,uint64,uint256,uint256))[],uint64,uint256))" $(cast call --rpc-url $ETH_RPC_URL $CREDIT "getAccount(address)" $EVM_ADDRESS)This will return the following values:
(6, 4999999999999999454276000000000000000000 [4.999e39], 504150000000000000000000 [5.041e23], 0x0000000000000000000000000000000000000000, 7200, [(0x90F79bf6EB2c4f870365E785982E1f101E93b906, (12345000000000000000000 [1.234e22], 987654321 [9.876e8], 11722 [1.172e4], 0, 0))], [], 86400 [8.64e4], 4999999984799342175554 [4.999e21])
Which maps to the Account struct:
struct Account {
uint256 capacityUsed; // 6
uint256 creditFree; // 4999999999999999454276000000000000000000
uint256 creditCommitted; // 504150000000000000000000
address creditSponsor; // 0x0000000000000000000000000000000000000000 (null)
uint64 lastDebitEpoch; // 7200
Approval[] approvalsTo; // See Approval struct below
Approval[] approvalsFrom; // [] (empty)
uint64 maxTtl; // 86400
uint256 gasAllowance; // 4999999984799342175554
}The approvals array is empty if no approvals have been made. However, our example does have
approvals authorized. We can expand this to be interpreted as the following:
struct Approval {
address addr; // 0x90F79bf6EB2c4f870365E785982E1f101E93b906
CreditApproval approval; // See CreditApproval struct below
}
struct CreditApproval {
uint256 creditLimit; // 12345000000000000000000
uint256 gasFeeLimit; // 987654321
uint64 expiry; // 11722
uint256 creditUsed; // 0
uint256 gasFeeUsed; // 0
}Due to intricacies with optional arguments in WASM being used in Solidity, you can interpret zero
values as null values in the structs above. That is, the example address has no restrictions on the
limit or expiry with using the delegated/approved credit from the owner's account.
Get credit stats
We can fetch the overall credit stats for the subnet with the following command:
cast abi-decode "getCreditStats()((uint256,uint256,uint256,uint256,uint64,uint64))" $(cast call --rpc-url $ETH_RPC_URL $CREDIT "getCreditStats()")This will return the following values:
(50000999975762821509530 [5e22], 50001000000000000000000 [5e22], 21600 [2.16e4], 24237178535296 [2.423e13], 1000000000000000000000000000000000000 [1e36], 10)
Which maps to the CreditStats struct:
struct CreditStats {
uint256 balance; // 50000999975762821509530
uint256 creditSold; // 50001000000000000000000
uint256 creditCommitted; // 21600
uint256 creditDebited; // 24237178535296
uint256 tokenCreditRate; // 1000000000000000000000000000000000000
uint64 numAccounts; // 10
}Get credit approval for an account
Get the credit approval from the address at EVM_ADDRESS to the address at RECEIVER_ADDR:
cast abi-decode "getCreditApproval(address,address)((uint256,uint256,uint64,uint256,uint256))" $(cast call --rpc-url $ETH_RPC_URL $CREDIT "getCreditApproval(address,address)" $EVM_ADDRESS $RECEIVER_ADDR)This will return the following values:
(100000000000000000000000000 [1e26], 1000, 7275, 0, 0)
Which maps to the CreditApproval struct:
struct CreditApproval {
uint256 creditLimit; // 100000000000000000000000000
uint256 gasFeeLimit; // 1000
uint64 expiry; // 7275
uint256 creditUsed; // 0
uint256 gasFeeUsed; // 0
}Get credit balance for an account
Fetch the credit balance for the address at EVM_ADDRESS:
cast abi-decode "getCreditBalance(address)((uint256,uint256,address,uint64,(address,(uint256,uint256,uint64,uint256,uint256))[],(address,(uint256,uint256,uint64,uint256,uint256))[],uint256))" $(cast call --rpc-url $ETH_RPC_URL $CREDIT "getCreditBalance(address)" $EVM_ADDRESS)This will return the following values:
(5001999999999998208637000000000000000000 [5.001e39], 518400000000000000000000 [5.184e23], 0x0000000000000000000000000000000000000000, 6932, [(0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65, (0, 0, 0, 0, 0))], 1)
Which maps to the Balance struct:
struct Balance {
uint256 creditFree; // 5001999999999998208637000000000000000000
uint256 creditCommitted; // 518400000000000000000000
address creditSponsor; // 0x0000000000000000000000000000000000000000 (null)
uint64 lastDebitEpoch; // 6932
Approval[] approvals; // See Approval struct below
}
struct Approval {
string to; // 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
CreditApproval approval; // See CreditApproval struct below
}
struct CreditApproval {
uint256 creditLimit; // 0
uint256 gasFeeLimit; // 0
uint64 expiry; // 0
uint256 creditUsed; // 0
uint256 gasFeeUsed; // 0
}Buy credit for an address
You can buy credit for your address with the following command, which will buy credit equivalent to
1 native subnet token (via msg.value) for the msg.sender:
cast send --rpc-url $ETH_RPC_URL $CREDIT "buyCredit()" --value 1ether --private-key $PRIVATE_KEYOr, you can buy credit for a specific EVM address with the following command:
cast send --rpc-url $ETH_RPC_URL $CREDIT "buyCredit(address)" $EVM_ADDRESS --value 1ether --private-key $PRIVATE_KEYApprove credit for an address
Approving credit has a few variations. The first variation is approving credit for the address
defined in the call, assuming the msg.sender is the owner of the credit. The RECEIVER_ADDR
address is the to we want to approve credit for (defined as
0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 above).
cast send --rpc-url $ETH_RPC_URL $CREDIT "approveCredit(address)" $RECEIVER_ADDR --private-key $PRIVATE_KEYThere also exists approveCredit(address,address) and approveCredit(address,address,address[]),
which inherently assumes a null value for the limit and ttl fields, and the order of the
addresses is from, to, and caller (for the latter variation). Here's an example using the
latter variation, effectively the same as the former due to the use of the zero address:
cast send --rpc-url $ETH_RPC_URL $CREDIT "approveCredit(address,address,address[])" $EVM_ADDRESS $RECEIVER_ADDR '[]' --private-key $PRIVATE_KEYIf, instead, we wanted to also restrict how the to can use the credit, we would set the caller
(e.g., a contract address at 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc):
cast send --rpc-url $ETH_RPC_URL $CREDIT "approveCredit(address,address,address[])" $EVM_ADDRESS $RECEIVER_ADDR '[0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc]' --private-key $PRIVATE_KEYThis would restrict the to to only be able to use the approved from address at the caller
address.
[!NOTE] The
callercan, in theory, be an EVM or WASM contract address. However, the logic
assumes only an EVM address is provided. Namely, it is generally possible to restrict the
callerto a specific WASM contract (e.g., bucket witht0...prefix), but the current Solidity
implementation does not account for this and only assumes an EVM address.
Lastly, if we want to include all of the optional fields, we can use the following command:
cast send --rpc-url $ETH_RPC_URL $CREDIT "approveCredit(address,address,address[],uint256,uint256,uint64)" $EVM_ADDRESS $RECEIVER_ADDR '[]' 100 100 3600 --private-key $PRIVATE_KEYThis includes the creditLimit field set to 100 credit, the gasFeeLimit set to 100 gas fee,
and the ttl set to 3600 seconds (1 hour). If either of these should instead be null, just set
them to 0.
Set credit sponsor for an address
cast send --rpc-url $ETH_RPC_URL $CREDIT "setCreditSponsor(address,address)" $EVM_ADDRESS $RECEIVER_ADDR --private-key $PRIVATE_KEYThis will set the credit sponsor for the from address to the sponsor address.
Revoke credit for an address
Revoking credit is the opposite of approving credit and also has a few variations. The simplest form
is revoking credit for the address defining in the call (to), which assumes the msg.sender is
the owner of the credit.
cast send --rpc-url $ETH_RPC_URL $CREDIT "revokeCredit(address)" $RECEIVER_ADDR --private-key $PRIVATE_KEYThe other variants are revokeCredit(address,address) and revokeCredit(address,address,address).
Just like approveCredit, the order is: from, to, and caller. Here's an example using the
latter variation:
cast send --rpc-url $ETH_RPC_URL $CREDIT "revokeCredit(address,address,address)" $EVM_ADDRESS $RECEIVER_ADDR 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc --private-key $PRIVATE_KEYThis would revoke the to's ability to use the from address at the caller address.
Buckets contract
You can interact with the existing buckets contract on the testnet via the address above. If you're
working on localnet, you'll have to deploy this yourself. Here's a quick one-liner to do so—also
setting the BUCKETS environment variable to the deployed address:
BUCKETS=$(forge script script/BucketManager.s.sol \
--tc DeployScript \
--sig 'run()' \
--rpc-url localnet_subnet \
--private-key $PRIVATE_KEY \
--broadcast \
-g 100000 \
| grep "0: contract BucketManager" | awk '{print $NF}')
Methods
The following methods are available on the credit contract, shown with their function signatures.
createBucket(): Create a bucket for the sender.createBucket(address): Create a bucket for the specified address.createBucket(address,(string,string)[]): Create a bucket for the specified address with
metadata.listBuckets(): List all buckets for the sender.listBuckets(address): List all buckets for the specified address.addObject(address,string,string,string,uint64,address): Add an object to a bucket and associated
object upload parameters. The first value is the bucket address, the subsequent values are all of
the "required" values inAddObjectParams(sourcenode ID,key,blobHash, andsize).addObject(address,(string,string,string,string,uint64,uint64,(string,string)[],bool,address)):
Add an object to a bucket (first value) and associated object upload parameters (second value) as
theAddObjectParamsstruct, described in more detail below.deleteObject(address,string,address): Remove an object from a bucket.getObject(address,string): Get an object from a bucket.queryObjects(address): Query the bucket (hex address) with no prefix (defaults to/delimiter
and the default offset and limit in the underlying WASM layer).queryObjects(address,string): Query the bucket with a prefix (e.g.,<prefix>/string value),
but no delimiter, offset, or limit.queryObjects(address,string,string): Query the bucket with a custom delimiter (e.g., something
besides the default/delimeter value), but default offset and limit.queryObjects(address,string,string,uint64): Query the bucket with a prefix and delimiter, but no
limit.queryObjects(address,string,string,uint64,uint64): Query the bucket with a prefix, delimiter,
offset, and limit.updateObjectMetadata(address,string,(string,string)[],address): Update the metadata of an
object.
Examples
Make sure you've already set the PRIVATE_KEY, EVM_ADDRESS, and ETH_RPC_URL environment
variables. Then, define a BUCKETS environment variable, which points to the bucket contract
deployment address. For example:
export BUCKETS=0x314512a8692245cf507ac6E9d0eB805EA820d9a8The account you use to create buckets should have the following:
- A RECALL token balance in the subnet (e.g., from the faucet at:
https://faucet.node-0.testnet.recall.network). You
can verify this with the Recall CLI:
recall account info - A credit balance in the subnet. You can verify this with
recall credit balance. If you don't
have credits, you can buy them with the Credits contract above, or run the
recall credit buy <amount>command.
Creating a bucket will cost native RECALL tokens, and writing to it will cost credit.
Create a bucket
To create a bucket, you can use the following command:
cast send --rpc-url $ETH_RPC_URL $BUCKETS "createBucket(address)" $EVM_ADDRESS --private-key $PRIVATE_KEYThis will execute an onchain transaction to create a bucket for the provided address. Alternatively,
you can create a bucket for the sender with the following command:
cast send --rpc-url $ETH_RPC_URL $BUCKETS "createBucket()" --private-key $PRIVATE_KEYTo create a bucket with metadata, you can use the following command, where each metadata value is a
KeyValue (a pair of strings) within an array—something like [("alias","foo")]:
cast send --rpc-url $ETH_RPC_URL $BUCKETS "createBucket(address,(string,string)[])" $EVM_ADDRESS '[("alias","foo")]' --private-key $PRIVATE_KEYList buckets
You can list buckets for a specific address with the following command. Note you can use the
overloaded listBuckets() to list buckets for the sender.
cast abi-decode "listBuckets(address)((uint8,address,(string,string)[])[])" $(cast call --rpc-url $ETH_RPC_URL $BUCKETS "listBuckets(address)" $EVM_ADDRESS)This will return the following output:
(0, 0xff000000000000000000000000000000000000ed, [("foo", "bar")])
Which maps to an array of the Machine struct:
struct Machine {
Kind kind; // See `Kind` struct below
address addr; // 0xff000000000000000000000000000000000000ed
KeyValue[] metadata; // See `KeyValue` struct below
}
struct Kind {
Bucket, // 0 == `Bucket`
Timehub, // 1 == `Timehub`
}
struct KeyValue {
string key; // "foo"
string value; // "bar"
}Add an object
Given a bucket address, you can read or mutate the objects in the bucket. First, we'll simply add an
object, setting BUCKET_ADDR to an existing bucket from the command above:
export BUCKET_ADDR=0xff0000000000000000000000000000000000008fAdding an object is a bit involved. You need to stage data offchain to a source bucket storage
node ID address, which will return the hashed value (blobHash) of the staged data and its
corresponding size in bytes. You then pass all of these as parameters when you add an object to
the bucket.
In the example below, we've already staged this data offchain and are using the following:
source: The node ID address (e.g.,cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq).blobHash: The hash of the staged data (e.g.,
rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmqis the base32 encoded blake3 hashed value
of our file contents, which contains the stringhello).recoveryHash: Blake3 hash of the metadata to use for object recovery (note: this is currently
hardcoded, so you can pass an empty string value here).size: The size of the data in bytes (e.g.,6, which is the number of bytes in thehello
string).
We also include custom parameters for the bucket key, metadata, TTL, and overwrite flag:
key: The key to assign to the object in the bucket (hello/world).metadata: The metadata to assign to the object in the bucket ([("foo","bar")]).overwrite: The overwrite flag to assign to the object in the bucket (false).
This all gets passed as a single AddObjectParams struct to the add method:
struct AddObjectParams {
string source; // cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq
string key; // hello/world
string blobHash; // rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq
string recoveryHash; // (note: this is currently hardcoded to an empty string)
uint64 size; // 6
uint64 ttl; // 0 (which is interpreted as null)
KeyValue[] metadata; // [("foo","bar")]
bool overwrite; // false
}We then pass this as a single parameter to the add method:
cast send --rpc-url $ETH_RPC_URL $BUCKETS "addObject(string,(string,string,string,string,uint64,uint64,(string,string)[],bool))" $BUCKET_ADDR '("cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq","hello/world","rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq","",6,0,[("foo","bar")],false)' --private-key $PRIVATE_KEYAlternatively, to use the overloaded add method that has default values for the ttl, metadata,
and overwrite, you can do the following:
cast send --rpc-url $ETH_RPC_URL $BUCKETS "addObject(string,string,string,string,string,uint64)" $BUCKET_ADDR "cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq" "hello/world" "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq" "" 6 --private-key $PRIVATE_KEYIf you're wondering where to get the source storage bucket's node ID (the example's
cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq), you can find it with a curl request. On
localnet, this looks like the following:
curl http://localhost:8001/v1/node | jq '.node_id'Or on testnet, you'd replace the URL with public bucket API endpoint
https://objects.node-0.testnet.recall.network.
Delete an object
Similar to getting an object, you can delete an object with the following command,
specifying the bucket and key for the mutating transaction:
cast send --rpc-url $ETH_RPC_URL $BUCKETS "deleteObject(address,string)" $BUCKET_ADDR "hello/world" --private-key $PRIVATE_KEYGet an object
Getting a single object is similar to the response of query, except only a single object is
returned. Thus, the response simply includes a single value. The BUCKET_ADDR is the same one from
above.
cast abi-decode "getObject(address,string)((string,string,uint64,uint64,(string,string)[]))" $(cast call --rpc-url $ETH_RPC_URL $BUCKETS "getObject(address,string)" $BUCKET_ADDR "hello/world")This will return the following response:
("rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq", "utiakbxaag7udhsriu6dm64cgr7bk4zahiudaaiwuk6rfv43r3rq", 6, 103381 [1.033e5], [("foo","bar")])Which maps to the Value struct:
struct ObjectValue {
string blobHash; // "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq"
string recoveryHash; // "utiakbxaag7udhsriu6dm64cgr7bk4zahiudaaiwuk6rfv43r3rq"
uint64 size; // 6
uint64 expiry; // 103381
KeyValue[] metadata; // See `KeyValue` struct below
}
struct KeyValue {
string key; // "foo"
string value; // "bar"
}Query objects
We'll continue using the same BUCKET_ADDR from the previous examples.
cast abi-decode "queryObjects(address)(((string,(string,uint64,uint64,(string,string)[]))[],string[],string))" $(cast call --rpc-url $ETH_RPC_URL $BUCKETS "queryObjects(address)" $BUCKET_ADDR)This will return the following Query output:
([], ["hello/"], "")
Where the first array is an empty set of objects, and the second array is the common prefixes in the
bucket:
struct Query {
Object[] objects; // Empty array if no objects
string[] commonPrefixes; // ["hello/"]
string nextKey; // ""
}In the next example, we'll set PREFIX to query objects with the prefix hello/:
export PREFIX="hello/"Now, we can query for these objects with the following command:
cast abi-decode "queryObjects(address,string)(((string,(string,uint64,uint64,(string,string)[]))[],string[],string))" $(cast call --rpc-url $ETH_RPC_URL $BUCKETS "queryObjects(address,string)" $BUCKET_ADDR $PREFIX)This will return the following Query output:
([("hello/world", ("rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq", 6, [("foo", "bar")]))], [], "")
Which maps to the following structs:
struct Query {
Object[] objects; // See `Object` struct below
string[] commonPrefixes; // Empty array if no common prefixes
string nextKey; // Null value (empty string `""`)
}
struct Object {
string key; // "hello/world"
ObjectState state; // See `ObjectState` struct below
}
struct ObjectState {
string blobHash; // "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq"
uint64 size; // 6
KeyValue[] metadata; // See `KeyValue` struct below
}
struct KeyValue {
string key; // "foo"
string value; // "bar"
}Update object metadata
You can update the metadata of an existing object with the following command:
cast send --rpc-url $ETH_RPC_URL $BUCKETS "updateObjectMetadata(address,string,(string,string)[],address)" $BUCKET_ADDR "hello/world" '[("alias","foo")]' $EVM_ADDRESS --private-key $PRIVATE_KEYBlobs contract
You can interact with the existing blobs contract on the testnet via the address above. If you're
working on localnet, you'll have to deploy this yourself. Here's a quick one-liner to do so—also
setting the BLOBS environment variable to the deployed address:
BLOBS=$(forge script script/BlobManager.s.sol \
--tc DeployScript \
--sig 'run()' \
--rpc-url localnet_subnet \
--private-key $PRIVATE_KEY \
--broadcast \
-g 100000 \
| grep "0: contract BlobManager" | awk '{print $NF}')
Methods
The following methods are available on the credit contract, shown with their function signatures.
Note that overloads are available for some methods, primarily, where the underlying WASM contract
accepts "optional" arguments. All of the method parameters and return types can be found in
util/CreditTypes.sol.
addBlob(AddBlobParams memory params): Store a blob directly on the network. This is described in
more detail below, and it involves a two-step approach with first staging data with the blob
storage node, and then passing related values onchain.deleteBlob(address,string,string,address): Delete a blob from the network, passing the sponsor's
address, the blob hash, and the subscription ID (either""if none was originally provided, or
the string that was chosen duringaddBlob).overwriteBlob(string,AddBlobParams memory): Overwrite a blob from the network, passing the old
blob hash, and the new blob parameters.getBlob(string): Get information about a specific blob at its blake3 hash.getBlobStatus(address,string,string): Get a blob's status, providing its credit sponsor (i.e.,
the account'saddress, oraddress(0)if null), its blake3 blob hash (the firststring
parameter), and its blob hash key (an empty string""to indicate the default, or the key used
upon creating the blob).getPendingBlobs(): Get the values of pending blobs across the network.getPendingBlobsCount(uint32): Get the number of pending blobs across the network, up to a limit.getPendingBytesCount(): Get the total number of pending bytes across the network.getStorageStats(): Get storage stats.getStorageUsage(address): Get storage usage for an address.getSubnetStats(): Get subnet stats.
Add a blob
Adding a blob is a bit involved. You need to stage data offchain to a source blob storage node ID
address, which will return the hashed value (blobHash) of the staged data and its corresponding
size in bytes. You then pass all of these as parameters when you add an object to the bucket.
In the example below, we've already staged this data offchain and are using the following:
sponsor: Optional sponsor address. E.g., if you have credits, you don't need to pass this, but
if someone has approve for you to use credits, you can specify the credit sponsor here.source: The storage node ID address (e.g.,
cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq).blobHash: The blake3 hash of the staged data (e.g.,
rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmqis the base32 encoded blake3 hashed value
of our file contents, which contains the stringhello).metadataHash: Blake3 hash of the metadata to use for blob recovery (hardcoded, so you can pass
an empty string value here).subscriptionId: Identifier used to differentiate blob additions for the same subscriber. You can
pass an empty string, indicating the default value (Default). Or, passing a string value, which
under the hood, will use this key value (Key(Vec<u8>)) for the hashmap key.size: The size of the data in bytes (e.g.,6, which is the number of bytes in thehello
string).ttl: Blob time-to-live epochs. If specified as0, the auto-debitor maintains about one hour of
credits as an ongoing commitment.from: The address of the account to use for the transaction.
This all gets passed as a single AddBlobParams struct to the addBlob method:
struct AddBlobParams {
address sponsor; // `address(0)` for default/null, or the credit sponsor's address
string source; // cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq
string blobHash; // rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq
string metadataHash; // (note: this is currently hardcoded to an empty string)
string subscriptionId; // use `""` for the default, or pass a string value
uint64 size; // 6
uint64 ttl; // 0 (which is interpreted as null)
address from; // 0x90F79bf6EB2c4f870365E785982E1f101E93b906
}We then pass this as a single parameter to the add method:
cast send --rpc-url $ETH_RPC_URL $BLOBS "addBlob((address,string,string,string,string,uint64,uint64,address))" "(0x0000000000000000000000000000000000000000,"cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq","rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq",\"\",\"\",6,0,$EVM_ADDRESS)" --private-key $PRIVATE_KEYTo include a custom subscription ID, you would replace the empty string (which indicates default)
in the call above, like so: (...,"rzgh...","","my_custom_id",6,0,...).
If you're wondering where to get the source storage bucket's node ID (the example's
cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq), you can find it with a curl request. On
localnet, this looks like the following:
curl http://localhost:8001/v1/node | jq '.node_id'Or on testnet, you'd replace the URL with public bucket API endpoint
https://objects.node-0.testnet.recall.network.
Delete a blob
You can a delete a blob you've created with the following, passing the sponsor's address (zero
address if null), the blob's blake3 hash, and the subscription ID (either the default empty string
"" or the string you passed during addBlob).
cast send --rpc-url $ETH_RPC_URL $BLOBS "deleteBlob(address,string,string)" 0x0000000000000000000000000000000000000000 "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq" "" --private-key $PRIVATE_KEYThis will emit a DeleteBlob event and delete the blob from the network.
Overwrite a blob
You can overwrite a blob you've created with the following, passing the old blob's blake3 hash, and
the new blob's parameters.
cast send --rpc-url $ETH_RPC_URL $BLOBS "overwriteBlob(string,(address,string,string,string,string,uint64,uint64,adderss))" "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq" '(0x0000000000000000000000000000000000000000,"cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq","rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq","","",6,0,0x90F79bf6EB2c4f870365E785982E1f101E93b906)' --private-key $PRIVATE_KEYThis will emit an OverwriteBlob event and overwrite the blob in the network.
Get a blob
cast abi-decode "getBlob(string)((uint64,string,(string,uint64)[],uint8))" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getBlob(string)" "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq")This will return the following response:
6, "utiakbxaag7udhsriu6dm64cgr7bk4zahiudaaiwuk6rfv43r3rq", [("1bce5a746a1134bf082baac80be951906bb406787e14c9fee0d7d8db0fc0b7f4", 98266 [9.826e4])], 2)Which maps to the Blob struct:
struct Blob {
uint64 size; // 6
string metadataHash; // "utiakbxaag7udhsriu6dm64cgr7bk4zahiudaaiwuk6rfv43r3rq"
Subscriber[] subscribers; // See `Subscriber` struct below
BlobStatus status; // 2 (Resolved)
}
struct Subscriber {
string subscriptionId; // "1bce5a746a1134bf082baac80be951906bb406787e14c9fee0d7d8db0fc0b7f4"
uint64 expiry; // 98266
}Get blob status
- Pass an address as the first field to represent the origin address that requested the blob.
- Provide the
blobHashblake3 value. - Also give the
subscriptionId, which uses the default empty value if you provide an empty string
"", or it can take the string that matches the blob'ssubscriptionIdupon creation.
cast abi-decode "getBlobStatus(address,string,string)(uint8)" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getBlobStatus(address,string,string)" $EVM_ADDRESS "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq" "")This will return the following response (either a 0 for Added, 1 for Pending, 2 for
Resolved, or 3 for Failed):
2Which maps to the BlobStatus enum:
enum BlobStatus {
Added, // 0
Pending, // 1
Resolved, // 2 -- the value above
Failed // 3
}Get added blobs
cast abi-decode "getAddedBlobs(uint32)((string,(address,string,string)[])[])" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getAddedBlobs(uint32)" 1)This returns the values of added blobs, up to the size passed as the parameter:
[("rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq", [(0x90F79bf6EB2c4f870365E785982E1f101E93b906, "default", "cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq")])]Which maps to an array of the BlobTuple struct:
struct BlobTuple {
string blobHash; // "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq"
BlobSourceInfo[] sourceInfo; // See `Subscriber` struct below
}
struct BlobSourceInfo {
address subscriber; // 0x90F79bf6EB2c4f870365E785982E1f101E93b906
string subscriptionId; // "default"
string source; // "cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq"
}Get pending blobs
cast abi-decode "getPendingBlobs(uint32)((string,(address,string,string)[])[])" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getPendingBlobs(uint32)" 1)This returns the values of pending blobs, up to the size passed as the parameter:
[("rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq", [(0x90F79bf6EB2c4f870365E785982E1f101E93b906, "default", "cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq")])]Which maps to an array of the BlobTuple struct:
struct BlobTuple {
string blobHash; // "rzghyg4z3p6vbz5jkgc75lk64fci7kieul65o6hk6xznx7lctkmq"
BlobSourceInfo[] sourceInfo; // See `Subscriber` struct below
}
struct BlobSourceInfo {
address subscriber; // 0x90F79bf6EB2c4f870365E785982E1f101E93b906
string subscriptionId; // "default"
string source; // "cydkrslhbj4soqppzc66u6lzwxgjwgbhdlxmyeahytzqrh65qtjq"
}Get pending blobs count
cast abi-decode "getPendingBlobsCount()(uint64)" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getPendingBlobsCount()")This returns the number of pending blobs:
123Get pending bytes count
cast abi-decode "getPendingBytesCount()(uint64)" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getPendingBytesCount()")This returns the total number of bytes that are pending network resolution:
987654321Get subnet stats
We can fetch the overall subnet stats with the following command:
cast abi-decode "getSubnetStats()((uint256,uint64,uint64,uint256,uint256,uint256,uint256,uint64,uint64,uint64,uint64,uint64,uint64))" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getSubnetStats()")This will return the following values:
(50000999980767329202072 [5e22], 10995116277754 [1.099e13], 6, 50001000000000000000000000000000000000000 [5e40], 1002060000000000000000000 [1.002e24], 62064000000000000000000 [6.206e22], 1000000000000000000000000000000000000 [1e36], 10, 1, 0, 0, 0, 0)
Which maps to the SubnetStats struct:
struct SubnetStats {
uint256 balance; // 50000000000000000000000
uint64 capacityFree; // 10995116277754
uint64 capacityUsed; // 6
uint256 creditSold; // 50001000000000000000000
uint256 creditCommitted; // 21156
uint256 creditDebited; // 25349384457000
uint256 tokenCreditRate; // 1000000000000000000000000000000000000
uint64 numAccounts; // 10
uint64 numBlobs; // 1
uint64 numResolving; // 0
uint64 bytesResolving; // 0
uint64 numAdded; // 0
uint64 bytesAdded; // 0
}Get storage stats
We can fetch the overall storage stats for the subnet with the following command:
cast abi-decode "getStorageStats()((uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64))" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getStorageStats()")This will return the following values:
(10995116277754 [1.099e13], 6, 1, 0, 10, 0, 0, 0)
Which maps to the StorageStats struct:
struct StorageStats {
uint64 capacityFree; // 10995116277754
uint64 capacityUsed; // 6
uint64 numBlobs; // 1
uint64 numResolving; // 0
uint64 numAccounts; // 10
uint64 bytesResolving; // 0
uint64 numAdded; // 0
uint64 bytesAdded; // 0
}Get storage usage for an account
Fetch the storage usage for the address at EVM_ADDRESS:
cast abi-decode "getStorageUsage(address)(uint256)" $(cast call --rpc-url $ETH_RPC_URL $BLOBS "getStorageUsage(address)" $EVM_ADDRESS)This will return the following values:
123
Testing
You can run all of the unit tests with the following command:
forge testOr run a specific test in the test directory:
forge test --match-path test/LibWasm.t.solFor the wrapper contracts, an naive "integration" test is provided in
test/scripts/wrapper_integration_test.sh. This assumes you have the localnet running, and it
deploys the contracts and runs through all of the methods described above via cast. You can run it
with the following command:
test/scripts/wrapper_integration_test.shIf everything is working, you should see All tests completed successfully logged at the end. But,
if there's an error (i.e., incompatability with the wrappers relative to the subnet's expectation),
the script will exit early.