Dapp ID Full Guide: creation, fees, centralized replenishment
What will you learn from this guide?
How to create your Dapp ID
How to interact with contracts inside a Dapp
How to interact with contracts of other Dapps
How to use the centralized replenishment mechanism for the Dapp ID contracts
Prerequisites
Configure CLI tool
In this guide, we will use the test network at shellnet.ackinacki.org.
We need to specify the blockchain endpoint for deployment:
tvm-cli config -g --url shellnet.ackinacki.orgThe command prints Succeeded. followed by the complete saved configuration. Only the relevant part is shown here:
Verify the saved endpoint:
The JSON output must contain:
The walkthrough below uses Shellnet. To deploy on Mainnet, select mainnet.ackinacki.org before funding the precomputed address and use that endpoint for deployment and all subsequent calls.
Create your first Dapp ID
There are two ways to deploy a contract, and the message type determines its Dapp ID:
External message
The contract starts a new Dapp ID and becomes its first, self-rooted contract. Its dapp_id is equal to its account_id.
Send SHELL to the self-rooted precomputed address with exchange flag 16 before deployment.
Internal message
The new contract joins the Dapp ID carried by the internal deployment message. Normally this is the sender's Dapp ID.
Include enough VMSHELL in the deployment message as initialBalance; separate pre-funding is not required.
If your Dapp consists of multiple contracts, deploy the first contract with an external message and deploy the other contracts from that root contract or its children with internal messages.
In this guide, we will use the helloWorld contract to demonstrate the features of a Dapp ID.
Prepare contract source code
Let's create a folder for our project and clone the repository with examples into it:
and copy the contracts folder from there:
Prepare the contract for signed calls and internal-message forwarding
Current sdk-examples/main already contains the required changes. Verify the cloned helloWorld.sol against the snippets below before compiling. If you use an older checkout and a snippet is missing, add it manually.
First, the source must include the public-key ABI header immediately after pragma AbiHeader expire;. Without it, a deployment signed with --sign fails with Error 621: The account has an invalid state:
The contract must also include the following forwarding method:
The public-key check restricts forwarding to signed calls from the key embedded in this contract. The method intentionally does not call getTokens(): it is needed to deploy DappConfig before centralized replenishment is available.
deployNewContract must use the same public-key check and be declared public view. Without this check, anyone can make the contract spend its balance to deploy an arbitrary stateInit:
Finally, verify that getTokens() uses the quiet replenishment instruction:
The q variant lets ordinary calls continue when DappConfig has not been deployed yet or has insufficient credit. The complete source at the beginning of this guide contains all the required code.
Compile
Compile the contract helloWorld using TVM Solidity compiler 0.81.0:
Expected output:
The commit and platform suffix may differ, but the output must start with sold 0.81.0.
The compiler produces no output on success. Verify the artifacts:
Example output:
TVM binary code of your contract is stored into helloWorld.tvc file.
Confirm that the generated ABI contains the forwarding method:
Expected output ends with:
An Invalid name: sendTransaction error means that helloWorld.sol was compiled without the forwarding method.
Fund the precomputed address
Every contract must have enough VMSHELL at its precomputed address to pay the deployment fee. In this first example, helloWorld will be deployed with an external message and its precomputed address will be self-rooted.
To do this, we first need to determine its address. Let's start by generating a seed phrase and keys for your contract:
Now let's generate the contract address using the keys obtained earlier:
The command prints two JSON fields: raw_address as 0:<account id> and dapp_account as <account id>::<account id>.
Example output from the Shellnet run used throughout this guide:
Your hashes will be different. All later example outputs use this tested address for consistency.
Send SHELL to the raw address 0:<HELLOWORLD_ACCOUNT_ID> with exchange flag 16. The flag converts the SHELL into VMSHELL at the destination before the contract is deployed. Flag 1 by itself does not perform this conversion.
Shellnet
The public Shellnet giver does not require a key. Replace <HELLOWORLD_ACCOUNT_ID> and run:
This sends 1000 SHELL and converts it into 1000 VMSHELL at the undeployed destination. The ecc["2"] field is the SHELL amount. The separate value field is VMSHELL attached to the delivery message and is used during message processing; do not add it to ecc["2"] when calculating the converted token amount. A successful response has "aborted": false and "exit_code": 0.
Example output, shortened to the fields you need to check:
Alternatively:
transfer SHELL from a previously deployed and funded Multisig Wallet as described in Fund an undeployed account.
See Get Test Tokens in Shellnet for other giver methods and token types.
Mainnet
The Shellnet giver is not available on Mainnet. First obtain SHELL as described in Buying SHELL. Then send it to the precomputed address with exchange flag 16 using either:
a previously deployed and funded Multisig Wallet;
Acki Nacki Wallet with Developer Mode enabled.
Check the state of the pre-deployed contract. It should be Uninit:
Expected fields before deployment:
Verify that its VMSHELL balance is greater than zero before deployment.
Deploy
This external deployment creates a new Dapp ID with helloWorld as its first contract. The deployment fee is paid from the VMSHELL already credited to the precomputed address.
Let's deploy helloWorld and create our first Dapp ID with this command. Use the account ID printed by genaddr as the destination Dapp ID:
Expected output, shortened:
Check the contract state again:
This time, acc_type must be Active, code_hash must be non-null, and dapp_id must equal <HELLOWORLD_DAPP_ID>:
View contract information with Explorer
Go to testnet Acki Nacki explorer and enter <YourAddress> in the search bar.
Open your account page. You will need it later to see its transactions and messages, that we will produce in the next steps.
Explore contract information with GraphQL
Go to GraphQL playground.
Enter the information in the left pane and click the "Run" button (replace the contract's address with the one you obtained in the previous steps).
Both arguments are bare 64-character hex ids, without 0: and without 0x: the two halves of <YourAddress>, which for a self-rooted contract are the same value.
Example response, with large BOC fields shortened for readability:
At the time of this Shellnet validation, GraphQL returned balance as a hexadecimal string: 0xe67b482a08 equals 989910805000 in decimal. The same balance is printed as a decimal string by tvm-cli account; do not compare the two textual representations directly.
The playground shows the same response in its right pane:
Run a getter
The helloWorld contract features a get-method: timestamp. Let's call it and check the result:
Example output:
timestamp is the value stored by the contract. state_timestamp is node metadata and is not the value returned by the getter.
Call a method on-chain
The helloWorld contract has a touch method. Let’s run it on-chain using the call command:
Expected output, shortened:
Call the get-method timestamp again to verify that the timestamp has been updated:
Add another contract to your Dapp ID
In our case, this can be done using the following function:
stateInit- the contract code plus data (tvc in base64);initialBalance- the amount of funds to transfer;payload- a tree of cells used as the body of the outbound internal message;
Let’s add another contract to our Dapp ID. For this, we’ll use a copy of the helloWorld contract and name it helloUniverse:
Now, let’s calculate the address of the helloUniverse contract using the existing key pair.
And we get the same address as the helloWorld contract.
The output confirms that both TVCs currently contain the same public key:
To avoid this, it’s essential to use a different key pair. Let’s generate a new seed phrase with a fresh pair of keys:
Let’s calculate the address and prepare the TVC file for the new contract:
Example output:
Save the new raw account ID as <HELLOUNIVERSE_ACCOUNT_ID>. Its final address after internal deployment will be <HELLOWORLD_DAPP_ID>::<HELLOUNIVERSE_ACCOUNT_ID>.
Do not fund this address separately. Unlike an external deployment, the internal deployment message can carry both stateInit and VMSHELL. The initialBalance argument used below supplies 10 VMSHELL to the new account and is enough for this example.
To deploy the new contract, prepare its stateInit and constructor message body.
To obtain the stateInit, execute the following command:
Since the result can be quite large, let’s save this value in a variable: HW_STATE_INIT.
This form works with both GNU and BSD/macOS base64.
Let’s generate the constructor message body for the internal deployment:
Expected output ends with:
We’ll need to place the Message body field value into the deployment payload.
Now we can call deployNewContract function.
In our case, the command will be as follows:
Expected output, shortened:
This deploys the new contract inside the existing Dapp ID through an internal message.
Check the contract state using its final Dapp ID address:
It must now be Active, and the returned dapp_id must equal <HELLOWORLD_DAPP_ID>.
The exact balance depends on execution fees, but it must be positive and lower than the 100000000000 nanoVMSHELL threshold used later in the replenishment example.
Call a contract inside Dapp ID
To transfer SHELL within the same Dapp ID, use the function sendShell:
dest- the target address to receive the transfer;value- the amount of SHELL tokens to transfer.
To transfer VMSHELL within the same Dapp ID, use the function sendVMShell:
dest- the target address to receive the transfer;amount- the amount of VMSHELL tokens to transfer.bounce- bounce flag: usetruefor an existing active recipient andfalsewhen intentionally creating an account.
Let's call the touch function in helloUniverse through the helloWorld contract.
But first, let's check the value of the timestamp variable in the helloUniverse contract.
Example output before the call:
To call the touch function in helloUniverse, we’ll invoke the callExtTouch method in helloWorld.
addr- is the address of the contract in which the method is called.
In our case, the command will be as follows:
Expected output, shortened:
Then, let's check if the timestamp has changed in the helloUniverse contract:
Output: The timestamp has changed.
Call a contract from another Dapp ID
Deploy helloWorld2 with an external message, just like the first root contract:
Example address output:
Save the account ID as both <HELLOWORLD2_ACCOUNT_ID> and <HELLOWORLD2_DAPP_ID>. Fund the raw address through the Shellnet giver with sendCurrencyWithFlag and flag 16:
Check that the giver response contains "aborted": false and "exit_code": 0, then deploy the contract:
Expected output, shortened:
The helloWorld and helloWorld2 contracts are deployed with different Dapp IDs.
Let’s check the current timestamp in the helloWorld2 contract:
Example output before the call:
To call the touch function in helloWorld2, we’ll invoke the callExtTouch method in helloWorld.
addr- is the address of the contract in which the method is called.
In our case, the command will be as follows:
Expected output, shortened:
Then, let's check if the timestamp has changed in the helloWorld2 contract:
Output: The timestamp has changed.
Centralized replenishment of contracts within a Dapp ID
Contracts grouped under one Dapp ID can replenish their VMSHELL balances from shared SHELL credit. The credit is stored in one DappConfig contract per Dapp ID and is consumed through the quiet TVM instruction gosh.mintshellq.
How it works:
During block assembly, the Block Keeper records calls to gosh.mintshellq. The requested amount is credited to the calling contract as VMSHELL and deducted from DappConfig.data.available_balance, which is denominated in nanoSHELL. A small credit remainder can remain after minting, so compare values before and after instead of asserting an exact final zero.
To ensure the system functions correctly and resources are managed automatically, follow these steps:
Step 1: Deploying the DappConfig contract
The DappConfig contract stores the amount of SHELL credit available for minting VMSHELL within a specific Dapp ID. Its address is deterministic, so only one DappConfig can exist for a given Dapp ID. DappConfig contracts do not have an owner, and anyone can fund them. For an explanation of how DappConfig replenishes Dapp account balances before fees are charged, see Fee Payment Process.
Actions to Perform:
To deploy the
DappConfigcontract, you need to know the Dapp ID. You can obtain it as follows:
Read the dapp_id field. In the tested example it is:
To deploy a
DappConfigcontract, calldeployNewConfigCustomthrough the self-rootedhelloWorldcontract. In this example itsaccount_idis equal to the required Dapp ID.Use the six-argument
sendTransactionforwarding method that you verified inhelloWorldbefore compiling it. Do not use a Multisig wallet or a child contract for this call.deployNewConfigCustompassesmsg.sender.valueas the new configuration's Dapp ID, so the caller's account ID must be the Dapp ID. That condition is true for the self-rootedhelloWorldcontract, but not for its children or for an unrelated wallet.That is, you first need to generate the message body by running the following command:
* abi DappRoot As a result, you will get:
Place the Message body value into the
payloadargument of the HelloWorldsendTransactionmethod and set the recipient to the DappRoot contract.Specify the amount of SHELL that becomes the initial
available_balancecredit.DappRootburns the received SHELL and passes the same numeric amount into the newDappConfig; the credit is not added to the account's VMSHELL balance.
Before sending any SHELL, calculate the deterministic DappConfig address:
Example output:
Save the raw account ID as <DAPPCONFIG_ACCOUNT_ID>, then perform the preflight account check:
If the command exits successfully and acc_type is Active, the configuration already exists: skip the funding and deployment commands below and continue with the getDetails verification. If the command exits with status 1 and its JSON Error starts with failed to get account, this Dapp ID has no configuration yet and it is safe to proceed. Do not match the rest of the error text: it comes from the HTTP layer and can change between versions.
The root contract must hold at least 100 SHELL before it can forward that amount to DappRoot. On Shellnet, fund it without exchange flag 16:
Here value is the VMSHELL attached to the delivery message; ecc["2"] is the separate SHELL amount. A successful response contains "aborted": false and "exit_code": 0. On Mainnet, transfer the SHELL to helloWorld from a funded wallet instead.
Now forward the deployment call to DappRoot:
Expected output, shortened:
Verify the deployed
DappConfig. The 100 SHELL sent during deployment is already usable credit.
The configuration belongs to the application Dapp, not to the system Dapp ID. Its canonical address is <HELLOWORLD_DAPP_ID>::<DAPPCONFIG_ACCOUNT_ID>. Verify it:
Example outputs, shortened:
Step 2: Enabling Automatic Replenishment
To automate the funding process, add balance check and token minting logic to your Dapp ID contracts.
Use the quiet TVM instruction gosh.mintshellq, which requests VMSHELL from the available credit in the DappConfig contract for this Dapp ID:
value- amount of nanoVMSHELL to mint
For example, let's use the getTokens() function in the HelloWorld contract:
This function mints 100 VMSHELL tokens automatically if the balance falls below the specified threshold.
Let's verify replenishment using helloUniverse, which was deployed with less than 100 VMSHELL. Check its balance:
The balance must be below 100000000000 nanoVMSHELL.
The exact balance depends on deployment and message-processing fees; this validation run started with 9994062000 nanoVMSHELL.
Using the getDetails() method, you can view the available balance of the DappConfig contract.
DappConfig belongs to the Dapp it provides credit for. Its canonical extended address is <HELLOWORLD_DAPP_ID>::<DAPPCONFIG_ACCOUNT_ID>.
Before the first successful mint, data.available_balance must be close to 100000000000 nanoSHELL.
Call touch in helloUniverse through the signed root method. Since the child balance is below 100 VMSHELL, getTokens() requests a replenishment:
Call the touch() function:
Expected output, shortened:
and check the contract balance:
The new balance is approximately the old balance plus 100 VMSHELL, plus the msg.value attached to the internal touch call, minus transaction fees. This is why the increase can be slightly greater or smaller than exactly 100000000000 nanoVMSHELL.
Run getDetails() again. data.available_balance decreases by approximately 100 SHELL:
Example output after minting:
Replenish DappConfig credit
An ordinary empty-payload SHELL transfer to DappConfig invokes its receive() function. The function adds the received ecc["2"] amount to data.available_balance, keeps or converts the small reserve needed by the contract, and burns excess physical SHELL. The credit remains recorded in available_balance even though it is not the contract's ECC balance.
On Shellnet, this tested giver call adds 10 SHELL of credit:
Expected transaction fields from the validation run:
In that run, getDetails() changed as follows:
The increase is approximately 10000000000 nanoSHELL. A small concurrent remainder adjustment is normal, so compare the readings rather than requiring an exact delta.
On Mainnet, send the same token amounts from a single-custodian v2 wallet:
The v2 wallet requires dapp_id for ABI encoding and event reporting; the transfer itself is still routed by dest.
Fees
When transferring messages between contracts under the same Dapp ID, fees are distributed as follows:
To create an outgoing message, payment is deducted from the sender’s balance.
For relaying a message, payment is taken either from the sender's balance or deducted from the message balance (
msg.value). The specific behavior depends on the flags set during transmission, as described here.Processing an incoming message is paid from the message balance (
msg.value) and, iftvm.accept()is used, from the recipient’s balance.
When transferring messages between contracts under different Dapp IDs, the entire amount of tokens specified in msg.value (VMSHELL) is nullified. In this case, the recipient contract must assume responsibility for executing the initiated transaction by calling tvm.accept() within the invoked function. Otherwise, the transaction will fail with the error Not enough funds.
Troubleshooting
Error 621: The account doesn't have a state during contract deployment
Description
When running the deploy command, you may encounter the following error:
Cause
This error means that the target account does not have an initialized state on the network.
✅ Solution
This can be done by calling the sendTransaction method with flag 16.
In this case, you transfer SHELL tokens, which are automatically converted into VMSHELL tokens and credited to the balance of the account you intend to deploy.
Example Command
The v2 wallet ABI requires the seventh dapp_id argument. For an undeployed self-rooted account, its future Dapp ID is the same as <ACCOUNT_ID>. The field is used for wallet event reporting; the actual destination is still selected by dest.
As a result, the account balance will be credited with approximately 5 VMSHELL. The separate value field is the VMSHELL carrier for message delivery; cc["2"] is the 5 SHELL converted by flag 16.
After the transaction is confirmed, you can safely run the deploy command again.
Error 621: The account has an invalid state
If the account is Uninit and has a positive VMSHELL balance, but deployment returns:
check the generated ABI:
It must contain pubkey:
If it does not, add pragma AbiHeader pubkey; to the Solidity source, compile again, run genaddr --save --setkey again, and fund the newly generated address. Changing the source changes the code hash and therefore the account ID; funds sent to the old address cannot be used to deploy the recompiled TVC.
Can't find account in shard state when reading an account
Description
A read request for an account that exists and is Active returns:
Cause
The account was requested under a Dapp ID it does not belong to and no redirect exists for the requested address. The response says nothing about the dapp_id being wrong.
The same account_id under two different dapp_id values:
✅ Solution
Request the account under its own Dapp ID:
a self-rooted contract — a Multisig Wallet, or any other contract deployed by an external message — has
dapp_idequal to itsaccount_id, and its canonical address is<account_id>::<account_id>;a contract deployed by an internal message from the root contract belongs to the Dapp ID of that root, so its canonical address is
<root_dapp_id>::<account_id>;
The dapp_id of an account is also returned by the GraphQL blockchain.account query shown in the Create your first Dapp ID section, and by a successful /v2/account response.
Last updated