neow3j v3.23.0
Neow3j is a development toolkit that provides easy and reliable tools to build Neo dApps and Smart Contracts using the Java platform (Java, Kotlin, Android). Check out neow3j.io for more detailed information on neow3j and the technical documentation.
#
1. SetupIf you haven't already set up your environment to use the neow3j library, you can check out our tutorial about setting up a neow3j project here.
#
2. NEP-17 OverviewThe NEP-17 is the fungible token standard on Neo N3. Have a look at its official documentation here.
#
3. Example NEP-17 ContractThe following example code represents a possible implementation for a token that supports the NEP-17 standard.
package io.neow3j.examples.contractdevelopment.contracts;
import io.neow3j.devpack.ByteString;import io.neow3j.devpack.Contract;import io.neow3j.devpack.Hash160;import io.neow3j.devpack.Helper;import io.neow3j.devpack.Runtime;import io.neow3j.devpack.Storage;import io.neow3j.devpack.StorageContext;import io.neow3j.devpack.StorageMap;import io.neow3j.devpack.annotations.DisplayName;import io.neow3j.devpack.annotations.ManifestExtra;import io.neow3j.devpack.annotations.OnDeployment;import io.neow3j.devpack.annotations.Permission;import io.neow3j.devpack.annotations.Safe;import io.neow3j.devpack.annotations.SupportedStandard;import io.neow3j.devpack.constants.CallFlags;import io.neow3j.devpack.constants.NeoStandard;import io.neow3j.devpack.contracts.ContractManagement;import io.neow3j.devpack.events.Event3Args;
/** * Be aware that this contract is an example. It has not been audited and should not be used in production. */@DisplayName("AxLabsToken")@ManifestExtra(key = "name", value = "AxLabsToken")@ManifestExtra(key = "author", value = "AxLabs")@SupportedStandard(neoStandard = NeoStandard.NEP_17)@Permission(contract = "*")public class FungibleToken {
static final int contractMapPrefix = 0; static final byte[] contractOwnerKey = new byte[]{0x00}; static final byte[] totalSupplyKey = new byte[]{0x01};
static final int assetMapPrefix = 1;
// region deploy, update, destroy
@OnDeployment public static void deploy(Object data, boolean update) { if (!update) { StorageContext ctx = Storage.getStorageContext(); // Set the contract owner. Hash160 initialOwner = (Hash160) data; if (!Hash160.isValid(initialOwner)) Helper.abort("Invalid deployment parameter"); Storage.put(ctx, contractOwnerKey, initialOwner); // Initialize the supply. int initialSupply = 200_000_000; Storage.put(ctx, totalSupplyKey, initialSupply); // Allocate all tokens to the contract owner. new StorageMap(ctx, assetMapPrefix).put(initialOwner, initialSupply); onTransfer.fire(null, initialOwner, initialSupply); if (new ContractManagement().getContract(initialOwner) != null) { Contract.call(initialOwner, "onNEP17Payment", CallFlags.All, new Object[]{null, initialSupply, null}); } } }
public static void update(ByteString script, String manifest) throws Exception { if (!Runtime.checkWitness(contractOwner(Storage.getReadOnlyContext()))) { throw new Exception("No authorization"); } new ContractManagement().update(script, manifest); }
public static void destroy() throws Exception { if (!Runtime.checkWitness(contractOwner(Storage.getReadOnlyContext()))) { throw new Exception("No authorization"); } new ContractManagement().destroy(); }
// endregion deploy, update, destroy // region NEP-17 methods
@Safe public static String symbol() { return "ALT"; }
@Safe public static int decimals() { return 2; }
@Safe public static int totalSupply() { return Storage.getInt(Storage.getReadOnlyContext(), totalSupplyKey); }
public static boolean transfer(Hash160 from, Hash160 to, int amount, Object[] data) throws Exception { if (!Hash160.isValid(from) || !Hash160.isValid(to)) { throw new Exception("The parameters 'from' and 'to' must be 20-byte addresses."); } if (amount < 0) { throw new Exception("The parameter 'amount' must be greater than or equal to 0."); } StorageContext ctx = Storage.getStorageContext(); if (amount > getBalance(ctx, from) || !Runtime.checkWitness(from)) { return false; }
if (from != to && amount != 0) { deductFromBalance(ctx, from, amount); addToBalance(ctx, to, amount); }
onTransfer.fire(from, to, amount); if (new ContractManagement().getContract(to) != null) { Contract.call(to, "onNEP17Payment", CallFlags.All, new Object[]{from, amount, data}); } return true; }
@Safe public static int balanceOf(Hash160 account) throws Exception { if (!Hash160.isValid(account)) { throw new Exception("The parameter 'account' must be a 20-byte address."); } return getBalance(Storage.getReadOnlyContext(), account); }
// endregion NEP-17 methods // region events
@DisplayName("Transfer") static Event3Args<Hash160, Hash160, Integer> onTransfer;
// endregion events // region custom methods
@Safe public static Hash160 contractOwner() { return new StorageMap(Storage.getReadOnlyContext(), contractMapPrefix).getHash160(contractOwnerKey); }
// endregion custom methods // region private helper methods
// When storage context is already loaded, this is a cheaper method than `contractOwner()`. private static Hash160 contractOwner(StorageContext ctx) { return new StorageMap(ctx, contractMapPrefix).getHash160(contractOwnerKey); }
private static void addToBalance(StorageContext ctx, Hash160 key, int value) { new StorageMap(ctx, assetMapPrefix).put(key.toByteArray(), getBalance(ctx, key) + value); }
private static void deductFromBalance(StorageContext ctx, Hash160 key, int value) { int oldValue = getBalance(ctx, key); new StorageMap(ctx, assetMapPrefix).put(key.toByteArray(), oldValue - value); }
private static int getBalance(StorageContext ctx, Hash160 key) { return new StorageMap(ctx, assetMapPrefix).getIntOrZero(key.toByteArray()); }
// endregion private helper methods
}
#
4. Contract BreakdownIn the following subsections, we'll be looking at each part of the NEP-17 example contract.
#
ImportsThe imports show the neow3j devpack classes that are used in the example contract. Check out neow3j devpack's javadoc for a full overview of classes and methods that are supported.
package io.neow3j.examples.contractdevelopment.contracts;
import io.neow3j.devpack.ByteString;import io.neow3j.devpack.Contract;import io.neow3j.devpack.Hash160;import io.neow3j.devpack.Helper;import io.neow3j.devpack.Runtime;import io.neow3j.devpack.Storage;import io.neow3j.devpack.StorageContext;import io.neow3j.devpack.StorageMap;import io.neow3j.devpack.annotations.DisplayName;import io.neow3j.devpack.annotations.ManifestExtra;import io.neow3j.devpack.annotations.OnDeployment;import io.neow3j.devpack.annotations.Permission;import io.neow3j.devpack.annotations.Safe;import io.neow3j.devpack.annotations.SupportedStandard;import io.neow3j.devpack.constants.CallFlags;import io.neow3j.devpack.constants.NeoStandard;import io.neow3j.devpack.contracts.ContractManagement;import io.neow3j.devpack.events.Event3Args;
#
Contract-specific InformationAnnotations on top of the smart contract's class represent contract-specific information. The following annotations can be used for in a smart contract:
@DisplayName
It specifies the contract's name. If this annotation is not present, the class name is used for the contract's name.
@ManifestExtra
Adds the provided key-value pair information in the manifest's extra
field. You can also use @ManifestsExtras
to gather multiple @ManifestExtra
annotations (results in the same as when using single @ManifestExtra
annotations).
@SupportedStandard
Sets the supportedStandards
field in the manifest. You can use neoStandard =
with the enum NeoStandard
to use an official standard (see here), or customStandard =
with a custom string value.
@Permission
Specifies, which third-party contracts and methods the smart contract is allowed to call. By default (i.e., if no permission annotation is set), the contract is not allowed to call any contract. Use contract =
and methods =
to specify, respectively, which contracts and methods are allowed. The permission in this example means that any contract and all its methods are allowed.
@DisplayName("AxLabsToken")@ManifestExtra(key = "name", value = "AxLabsToken")@ManifestExtra(key = "author", value = "AxLabs")@SupportedStandard(neoStandard = NeoStandard.NEP_17)@Permission(contract = "*")public class FungibleToken {
#
ConstantsYou can set a constant value for the contract by using final
variables. These values are always loaded when the contract is called and cannot be changed once the contract is deployed. If a final value does not include a method call (e.g., raw types, or a final String
value, such as "name"), then these values are inlined during compilation.
note
All contract constants and all methods must be static
(since the object-orientation of the JVM is different on the NeoVM).
tip
The contract owner of this example contract is fixed (i.e., it is a final
variable). If you intend to provide a way to change such a variable, you should not store it as a final
variable. Rather, you would store it as a value in the storage, which provides the possibility to be modified through a method.
static final int contractMapPrefix = 0;static final byte[] contractOwnerKey = new byte[]{0x00};static final byte[] totalSupplyKey = new byte[]{0x01};
#
DeployOnce a deployment transaction is made (containing the contract and other parameters), the contract data is first stored on the blockchain and then the native contract ContractManagement
calls the smart contract's deploy()
method. In neow3j, that method is marked with the annotation @OnDeployment
. In the example, when the smart contract is deployed, the initialSupply
is set to 200'000'000 and it is allocated to the smart contract's owner.
@OnDeploymentpublic static void deploy(Object data, boolean update) { if (!update) { StorageContext ctx = Storage.getStorageContext(); // Set the contract owner. Hash160 initialOwner = (Hash160) data; if (!Hash160.isValid(initialOwner)) Helper.abort("Invalid deployment parameter"); Storage.put(ctx, contractOwnerKey, initialOwner); // Initialize the supply. int initialSupply = 200_000_000; Storage.put(ctx, totalSupplyKey, initialSupply); // Allocate all tokens to the contract owner. new StorageMap(ctx, assetMapPrefix).put(initialOwner, initialSupply); onTransfer.fire(null, initialOwner, initialSupply); if (new ContractManagement().getContract(initialOwner) != null) { Contract.call(initialOwner, "onNEP17Payment", CallFlags.All, new Object[]{null, initialSupply, null}); } }}
#
Update and DestroyIn order to update the contract, the following method first checks that the contract owner witnessed the transaction and then the native ContractManagement.update()
method is called. When updating a smart contract, you can change the smart contract's code and its manifest. This means that you can update how the contract programmatically manages its storage context.
note
Additionally to changing the smart contract's script and manifest, the method ContractManagement.update()
eventually calls the smart contract's deploy()
method (shown above) with the boolean update
set to true.
public static void update(ByteString script, String manifest) throws Exception { if (!Runtime.checkWitness(contractOwner(Storage.getReadOnlyContext()))) { throw new Exception("No authorization"); } new ContractManagement().update(script, manifest);}
The example contract also provides the option to destroy the smart contract. As well as the update()
method, it first verifies that the contract owner witnessed the transaction and then calls the method ContractManagement.destroy()
method.
caution
When the native method ContractManagement.destroy()
is called from a smart contract, the whole smart contract's storage context is erased, and the contract can no longer be used.
public static void destroy() throws Exception { if (!Runtime.checkWitness(contractOwner(Storage.getReadOnlyContext()))) { throw new Exception("No authorization"); } new ContractManagement().destroy();}
#
NEP-17 MethodsThe required NEP-17 methods are implemented as follows. If a method does not change the state of the contract (i.e., it is just used for reading), it can be annotated with the @Safe
annotation. Out of the NEP-17 methods, only the transfer()
method should be writing to the contract and is thus not annotated as safe.
@Safepublic static String symbol() { return "ALT";}
@Safepublic static int decimals() { return 2;}
@Safepublic static int totalSupply() { return Storage.getInt(Storage.getReadOnlyContext(), totalSupplyKey);}
public static boolean transfer(Hash160 from, Hash160 to, int amount, Object[] data) throws Exception { if (!Hash160.isValid(from) || !Hash160.isValid(to)) { throw new Exception("The parameters 'from' and 'to' must be 20-byte addresses."); } if (amount < 0) { throw new Exception("The parameter 'amount' must be greater than or equal to 0."); } StorageContext ctx = Storage.getStorageContext(); if (amount > getBalance(ctx, from) || !Runtime.checkWitness(from)) { return false; }
if (from != to && amount != 0) { deductFromBalance(ctx, from, amount); addToBalance(ctx, to, amount); }
onTransfer.fire(from, to, amount); if (new ContractManagement().getContract(to) != null) { Contract.call(to, "onNEP17Payment", CallFlags.All, new Object[]{from, amount, data}); } return true;}
@Safepublic static int balanceOf(Hash160 account) throws Exception { if (!Hash160.isValid(account)) { throw new Exception("The parameter 'account' must be a 20-byte address."); } return getBalance(Storage.getReadOnlyContext(), account);}
#
EventsThe NEP-17 standard requires an event Transfer
that contains the values from
, to
, and amount
. For this, the class Event3Args
can be used with the annotation @DisplayName
to set the event's name that will be shown in the manifest and notifications when it has been fired.
@DisplayName("Transfer") static Event3Args<Hash160, Hash160, Integer> onTransfer;
An event variable can effectively fire an event by using the fire()
method with the corresponding arguments. For example, the Transfer
event (represented by the onTransfer
variable) should be fired whenever a transfer happens.
onTransfer.fire(from, to, amount);
#
Custom MethodsThe example contract contains two custom methods that are not specified in the NEP-17 standard. The method contractOwner()
simply returns the script hash of the contract owner.
@Safepublic static Hash160 contractOwner() { return new StorageMap(Storage.getReadOnlyContext(), contractMapPrefix).getHash160(contractOwnerKey);}
#
Private Helper MethodsPrivate methods can be used to simplify and make the smart contract more readable. The following private methods are used in the NEP-17 example contract.
// When storage context is already loaded, this is a cheaper method than `contractOwner()`.private static Hash160 contractOwner(StorageContext ctx) { return new StorageMap(ctx, contractMapPrefix).getHash160(contractOwnerKey);}
private static void addToBalance(StorageContext ctx, Hash160 key, int value) { new StorageMap(ctx, assetMapPrefix).put(key.toByteArray(), getBalance(ctx, key) + value);}
private static void deductFromBalance(StorageContext ctx, Hash160 key, int value) { int oldValue = getBalance(ctx, key); new StorageMap(ctx, assetMapPrefix).put(key.toByteArray(), oldValue - value);}
private static int getBalance(StorageContext ctx, Hash160 key) { return new StorageMap(ctx, assetMapPrefix).getIntOrZero(key.toByteArray());}
#
5. Compile the ContractThe contract can be compiled using the gradle plugin. First, set the className
in the file gradle.build
to the contract's class name. Then, the gradle task neow3jCompile
can be executed from the project's root path to compile the contract.
./gradlew neow3jCompile
The output is then accessible in the folder ./build/neow3j
, and should contain the following three files:
AxLabsToken.manifest.jsonAxLabsToken.nefAxLabsToken.nefdbgnfo
note
The filenames can deviate according to what the contract's name is. See here.
Now, the contract's .manifest.json
and .nef
files can be used to deploy the contract. Neow3j's SDK can be used to do so. Check out the example here about how to deploy a contract with its manifest and nef files.
#
AboutFeel free to report any issues that might arise. Open an issue here to help us directly including it in our backlog.