TOMB compiler for Phantasma platform.
It lets you write custom smart contracts, tokens and smart NFTs.
TOMB is provided as a pre-built .NET executable.
It works in Windows, Linux and OSX, but you will need to have the .NET runtimes installed.
To compile your TOMB scripts, open a terminal and execute TombCompiler, passing your file as argument.
TombCompiler my_contract.tomb
Option | Description |
---|---|
-output | Specify the output path for compiled files. By default those files will be written to a Output directory inside the source file location. |
-protocol | Sets a specific protocol version for contracts. By default, it will use the LatestKnownProtocol constant obtained for Phantasma Chain package. |
-debug | Will print some extra debug info. |
TOMB is also optionally available as a C# library via Nuget.
The package is called 'Phantasma.TOMB'.
In order to compile scripts from within C#, install Phantasma.TOMB package then do something like:
using Phantasma.Tomb.Compilers;
public class MyCompiler
{
static void Main(string[] args)
{
var sourceFileName = args[0];
var sourceCode = File.ReadAllText(sourceFilePath);
var compiler = new TombLangCompiler(); // You could also use SolidityCompiler or your own custom compiler class
var modules = compiler.Process(sourceCode);
// This will generate 3 files for each compiled module
foreach (var module in modules)
{
File.WriteAllText(module.Name + ".asm", module.asm);
File.WriteAllBytes(module.Name + ".pvm", module.script);
File.WriteAllBytes(module.Name + ".abi", module.abi.ToByteArray());
}
}
}
TOMB generates code that runs in the PhantasmaVM, and supports multiple programming languages.
Language | File Extension | Status | Description |
---|---|---|---|
TOMB lang | .tomb | Fully working | The original language supported by TOMB (the rest of this document samples use it) |
Solidity | .tomb | Working (around 70% features support) | The language originally created for Ethereum EVM |
- Smart contracts and Non-contract Scripts (eg: transactions, raw invokes)
- Numbers, strings, bools, timestamps, addresses, hashes
- Constants
- Enums
- Global and local variables
- Array indexing
- Bitshifting and logical operators
- Contract constructors, methods and triggers
- Contract public methods
- Return values
- Collections (Maps and lists)
- Generic types
- If ... Else
- While ... and Do ... While loops
- For.. Loops
- Switch .. case
- Break and Continue
- Throw Exceptions
- Uninitialized globals validation
- Custom events
- Interop and Contract calls
- Inline asm
- Type inference in declarations
- Structs
- Import libraries (Runtime, Leaderboard, Token, etc)
- Comments (single and multi line)
- Contract tasks
- ABI generation
- Postfix operators (++, --)
- External library declarations
- Try .. Catch
- More...
- Warnings
- Debugger support
- Call a function from anywhere in the code
- Create Classes that can be manipulated
- Change Struct values of an instanciated struct without needing to recreated
- Add && and || (currently its or, and)
- Better support for Arrays, methods like, Array.shuffle() | Array.push() | Array.add() | Array.pop() | Array.shift()
- Better Math Library, implement methods like, Math.Ceil() | Math.floor()
- Multiple file support (Before compiling it, to make the code easier to write.)
- Implement a null types
For developers who used previous TOMB versions, the assigment operator has been changed from := to = Also the operators "and" and "or" were changed to "&&" and "||" respectively.
Different data types are recognized by the compiler.
Type | Example | |
---|---|---|
Number | 123 | |
Decimal | 0.123 | Where X is the number of maximum decimal places |
Bool | false | |
String | "hello" | |
Timestamp | no literal support, use either Time.now or Time.unix | |
Byte array | 0xFAFAFA2423424 | |
Address | @P2K6p3VzyRhxqHE2KcNV2B3QjVrv5ekvWPZLevteDoBQTzA or @null | |
Hash | #E3FE7BB73996CF7057913BD916F1B07AC0EAB4916DF3BCBDC221829F5CBEA9AF | |
Compiler macro | $SOMETHING |
The following libraries can be imported into a contract.
Method | Return type | Description |
---|---|---|
Call.interop(...:Generic) | Any | TODO |
Call.contract(contractName, method:String, ...:Generic) | Any | This is used to call another contract with a specified method. |
Call.method(...:Generic) | Any | To call a method inside the contract, but instead of using "this.methodName()" |
Method | Return type | Description |
---|---|---|
Runtime.expect(condition:Bool, error:String) | None | To check if the condition is true, else returns the error, and the code below, won't be executed. |
Runtime.log(message:String) | None | To log a message. |
Runtime.isWitness(address:Address) | Bool | Check if is they the owner of that given Address. |
Runtime.isTrigger() | Bool | Returns true if a trigger and false otherwise. |
Runtime.transactionHash() | Hash | TODO |
Runtime.deployContract(from:Address, contract:Module) | None | TODO |
Runtime.upgradeContract(from:Address, contract:Module) | None | TODO |
Runtime.gasTarget() | Address | TODO |
Runtime.context() | String | TODO |
Runtime.previousContext() | String | TODO |
Method | Return type | Description |
---|---|---|
Math.min(a:Number, b:Number) | Number | Returns smallest of two numbers |
Math.max(a:Number, b:Number) | Number | Returns largest of two numbers |
Method | Return type | Description |
---|---|---|
Token.create(from:Address, symbol:String, name:String, maxSupply:Number, decimals:Number, flags:Number, script:Bytes, abi:Bytes) | None | Method used to create a new token. |
Token.exists(symbol:String) | Bool | Returns true if the token exists and false otherwise. |
Token.getDecimals(symbol:String) | Number | Returns the number of decimals of the token. |
Token.getFlags(symbol:String) | Enum | Returns an Enum with the TokenFlag of the token. |
Token.transfer(from:Address, to:Address, symbol:String, amount:Number) | None | Transfers tokens from one Address, to another address with a specific symbol and the desired amount. |
Token.transferAll(from:Address, to:Address, symbol:String) | None | Transfer all tokens from one Address, to another address with a specific symbol. |
Token.mint(from:Address, to:Address, symbol:String, amount:Number) | None | Mints tokens from one Address, to another address with a specific symbol and the desired amount. |
Token.write(from:Address, symbol:String, tokenID:Number, ram:Any) | None | Updates the Token/NFT RAM with the given RAM. |
Token.burn(from:Address, symbol:String, amount:Number) | None | Burn tokens from the given address, given symbol and the desired amount. |
Token.swap(targetChain:String, source:Address, destination:Address, symbol:String, amount:Number) | None | TODO |
Token.getBalance(from:Address, symbol:String) | Number | Returns the token balance of the specified address. |
Token.isMinter(address:Address, symbol:String) | Bool | Returns true if the token is a Minter and false otherwise. |
Token.availableSymbols() | Array | Returns list with symbols of all deployed fungible tokens. |
Method | Return type | Description |
---|---|---|
NFT.transfer(from:Address, to:Address, symbol:String, id:Number) | None | Transfer an NFT from one address to another, by the nftID and symbol. |
NFT.mint(from:Address, to:Address, symbol:String, rom:Any, ram:Any, seriesID:Number) | None | Mint an NFT from one address to another, with the specific ROM and RAM, and the seriesID. |
NFT.write(from:Address, symbol:String, tokenID:Number, ram:Any) | None | Write NFT is to update the RAM inside the NFT by the nftID. |
NFT.burn(from:Address, symbol:String, id:Number) | None | Burn the nft by the NFTID. |
NFT.infuse(from:Address, symbol:String, id:Number, infuseSymbol:String, infuseValue:Number) | None | Infuse the NFT with other tokens/NFT with a given amount. |
NFT.createSeries(from:Address, symbol:String, seriesID:Number, maxSupply:Number, mode:Enum, nft:Module) | None | Creates a series of NFTs. |
NFT.readROM(symbol:String, id:Number) | T | Returns the ROM by the NFTID. |
NFT.readRAM(symbol:String, id:Number) | T | Returns the RAM by the NFTID. |
NFT.getInfusions(symbol:String, id:Number) | Array | Returns list with all tokens infused into a specific token. |
NFT.getOwnerships(from:Address, symbol:String) | Array | Returns list with token ids of NFTs owned by the specified address and symbol. |
NFT.availableSymbols() | Array | Returns list with symbols of all deployed non-fungible tokens. |
Method | Return type | Description |
---|---|---|
Account.getName(from:Address) | String | TODO |
Account.getLastActivity(from:Address) | Timestamp | TODO |
Account.registerName(target:Address, name:String) | None | TODO |
Account.unregisterName(target:Address) | None | TODO |
Account.registerScript(target:Address, script:Bytes, abiBytes:Bytes) | None | TODO |
Account.hasScript(address:Address) | Bool | TODO |
Account.lookUpAddress(target:Address) | String | TODO |
Account.lookUpScript(target:Address) | Bytes | TODO |
Account.lookUpABI(target:Address) | Bytes | TODO |
Account.lookUpName(name:String) | Address | TODO |
Account.migrate(from:Address, target:Address) | None | TODO |
Method | Return type | Description |
---|---|---|
Organization.create(from:Address, id:String, name:String, script:Bytes) | None | TODO |
Organization.addMember(from:Address, name:String, target:Address) | None | TODO |
Method | Return type | Description |
---|---|---|
Oracle.read(url:String) | None | TODO |
Oracle.price(symbol:String) | None | Retruns the price of the given symbol. |
Oracle.quote(baseSymbol:String, quoteSymbol:String, amount:Number) | None | Returns the price converted to the given quote symbol with the given amount. |
Method | Return type | Description |
---|---|---|
Storage.read(contract:String, field:String, type:Number) | Any | TODO |
Storage.write(field:String, value:Any) | None | TODO |
Storage.delete(field:String) | None | TODO |
Storage.calculateStorageSizeForStake(stakeAmount:Number) | Number | TODO |
Storage.createFile(target:Address, fileName:String, fileSize:Number, contentMerkle:Bytes, encryptionContent:Bytes) | None | TODO |
Storage.hasFile(target:Address, hash:Hash) | Bool | TODO |
Storage.addFile(from:Address, target:Address, archiveHash:Hash) | None | TODO |
Storage.deleteFile(from:Address, targetHash:Hash) | None | TODO |
Storage.hasPermission(externalAddr:Address, target:Address) | Bool | TODO |
Storage.addPermission(from:Address, externalAddr:Address) | None | TODO |
Storage.deletePermission(from:Address, externalAddr:Address) | None | TODO |
Storage.migratePermission(target:Address, oldAddr:Address, newAddr:Address) | None | TODO |
Storage.migrate(from:Address, target:Address) | None | TODO |
Storage.getUsedSpace(from:Address) | Number | TODO |
Storage.getAvailableSpace(from:Address) | Number | TODO |
Storage.GetUsedDataQuota(address:Address) | Number | TODO |
Storage.writeData(target:Address, key:Bytes, value:Bytes) | None | TODO |
Storage.deleteData(target:Address, key:Bytes) | None | TODO |
Method | Return type | Description |
---|---|---|
Array.get(array:Any, index:Number) | Generic<0> | Returns the element of an array at the given index. |
Array.set(array:Any, index:Number, value:Generic<0>) | None | Set the element of an array at the given index with the given value. |
Array.remove(array:Any, index:Number) | None | Removes the element of an array at the given index. |
Array.clear(array:Any) | None | Clears all the Array entries. |
Array.count(array:Any) | Number | Returns the number of elements in the Array. |
Method | Return type | Description |
---|---|---|
Utils.contractAddress(name:String) | Address | TODO |
Method | Return type | Description |
---|---|---|
Leaderboard.create(from:Address, boardName:String, capacity:Number) | None | To create a new leaderboard by the given address with a board name and a given capacity. |
Leaderboard.getAddress(boardName:String, index:Number) | Address | Returns the address of the leaderboard, by the given board name and index. |
Leaderboard.getScoreByIndex(boardName:String, index:Number) | Number | Returns the score that position, by the given board name and index. |
Leaderboard.getScoreByAddress(boardName:String, target:Address) | Number | Returns the score of the address, by the given board name and the target address. |
Leaderboard.getSize(boardName:String) | Number | Returns the size of the Leaderboard, by the given board name. |
Leaderboard.insert(from:Address, target:Address, boardName:String, score:Number) | None | To insert a score into the Leaderboard, by the target address, board name and score. |
Leaderboard.reset(from:Address, boardName:String) | None | To reset the Leaderboard, by the address and board name. |
Method | Return type | Description |
---|---|---|
Market.sell(from:Address, baseSymbol:String, quoteSymbol:String, tokenID:Number, price:Number, endDate:Timestamp) | None | To sell an NFT, by the tokenID. |
Market.buy(from:Address, symbol:String, tokenID:Number) | None | To buy an NFT, by the tokenID. |
Market.cancel(symbol:String, tokenID:Number) | None | To cancel a Sell. |
Market.hasAuction(symbol:String, tokenID:Number) | Bool | Returns true if has an auction for the given tokenID. |
Market.bid(from:Address, symbol:String, tokenID:Number, price:Number, buyingFee:Number, buyingFeeAddress:Address) | None | To bid for the given tokenID. |
Market.listToken(from:Address, baseSymbol:String, quoteSymbol:String, tokenID:Number, price:Number, endPrice:Number, startDate:Timestamp, endDate:Timestamp, extensionPeriod:Number, typeAuction:Number, listingFee:Number, listingFeeAddress:Address) | None | To list the token by the TokenID. |
Market.editAuction(from:Address, baseSymbol:String, quoteSymbol:String, tokenID:Number, price:Number, endPrice:Number, startDate:Timestamp, endDate:Timestamp, extensionPeriod:Number) | None | To edit the auction for the given tokenID. |
Method | Return type | Description |
---|---|---|
Crowdsale.create(from:Address, name:String, SaleFlags flags, startDate:Timestamp, endDate:Timestamp, sellSymbol:String, receiveSymbol:String, price:Number, globalSoftCap:Number, globalHardCap:Number, userSoftCap:Number, userHardCap:Number) | Hash | Returns a Hash of the sale created. |
Crowdsale.isSaleActive(saleHash:Hash) | bool | Returns true if the sale is active, false otherwise. |
Crowdsale.GetSaleParticipants(saleHash:Hash) | Address[] | Returns an Array of the addressess in by the saleHash. |
Crowdsale.getSaleWhitelists(saleHash:Hash) | Address[] | Retruns the list of addresses whitelisted for the sale, by the saleHash. |
Crowdsale.isWhitelisted(saleHash:Hash, address:Address) | Bool | Returns true if the user is whitelisted for the sale, false otherwise. |
Crowdsale.addToWhitelist(saleHash:Hash, target:Address) | None | To add a Address to the participate in the sale, by the saleHash and target address. |
Crowdsale.removeFromWhitelist(saleHash:Hash, target:Address) | None | To remove a Address from the participate in the sale, by the saleHash and target address. |
Crowdsale.getPurchasedAmount(saleHash:Hash, address:Address) | Number | Returns the Purchased amount. |
Crowdsale.getSoldAmount(saleHash:Hash) | Number | Returns the amount that the sale has been sold. |
Crowdsale.purchase(from:Address, saleHash:Hash, quoteSymbol:string, quoteAmount:Number) | None | To purchase the sale. |
Crowdsale.closeSale(from:Address, saleHash:Hash) | None | To close a given saleHash. |
Crowdsale.getLatestSaleHash() | Hash | Returns the last saleHash. |
Crowdsale.EditSalePrice(saleHash:Hash, price:Number) | None | To edit the sale price by the saleHash. |
Method | Return type | Description |
---|---|---|
Stake.getMasterThreshold() | Number | TODO |
Stake.isMaster(address:Address) | bool | TODO |
Stake.getMasterCount() | Number | TODO |
Stake.getClaimMasterCount(claimDate:Timestamp) | Number | TODO |
Stake.getMasterClaimDate(claimDistance:Number) | Timestamp | TODO |
Stake.getMasterDate(target:Address) | Timestamp | TODO |
Stake.getMasterClaimDateFromReference(claimDistance:Number, referenceTime:Timestamp) | Timestamp | TODO |
Stake.getMasterRewards(address:Address) | Number | TODO |
Stake.migrate(from:Address, to:Address) | None | TODO |
Stake.masterClaim(from:Address) | None | TODO |
Stake.stake(from:Address, stakeAmount:Number) | None | TODO |
Stake.unstake(from:Address, unstakeAmount:Number) | None | TODO |
Stake.getTimeBeforeUnstake(from:Address) | Number | TODO |
Stake.getStakeTimestamp(from:Address) | Timestamp | TODO |
Stake.getUnclaimed(from:Address) | Number | TODO |
Stake.claim(from:Address, stakeAddress:Address) | None | TODO |
Stake.getStake(address:Address) | Number | TODO |
Stake.getStorageStake(address:Address) | Number | TODO |
Stake.fuelToStake(fuelAmount:Number) | Number | TODO |
Stake.stakeToFuel(stakeAmount:Number) | Number | TODO |
Stake.getAddressVotingPower(address:Address) | Number | TODO |
Method | Return type | Description |
---|---|---|
Governance.hasName(name:String) | bool | TODO |
Governance.gasValue(name:String) | bool | TODO |
Governance.createValue(name:String, initial:Number, serializedConstraints:Bytes) | None | TODO |
Governance.getValue(name:String) | Number | TODO |
Governance.setValue(name:String, value:Number) | None | TODO |
Method | Return type | Description |
---|---|---|
Relay.getBalance(from:Address) | Number | TODO |
Relay.getIndex(from:Address, to:Address) | Number | TODO |
Relay.getTopUpAddress(from:Address) | Address | TODO |
Relay.openChannel(from:Address, publicKey:Bytes) | None | TODO |
Relay.getKey(from:Address) | Bytes | TODO |
Relay.topUpChannel(from:Address, count:Number) | None | TODO |
Relay.settleChannel(receipt:RelayReceipt) | None | TODO |
Method | Return type | Description |
---|---|---|
Mail.pushMessage(from:Address, target:Address, archiveHash:Hash) | None | To push a message to the target user mailbox. |
Mail.domainExists(domainName:String) | Bool | Returns true if the specified domain exists, false otherwise. |
Mail.registerDomain(from:Address, domainName:String) | None | To register a domain, from an address and a domain name. |
Mail.unregisterDomain(domainName:String) | None | To unregister a domain name. |
Mail.migrateDomain(domainName:String, target:Address) | None | To migrate a domain to a new Address. |
Mail.joinDomain(from:Address, domainName:String) | None | To a user join a domain. |
Mail.leaveDomain(from:Address, domainName:String) | None | To a user leave a domain. |
Mail.getUserDomain(target:Address) | String | To the the user domain. |
Method | Return type | Description |
---|---|---|
Time.now() | Timestamp | Returns the current time. |
Time.unix(value:Number) | Timestamp | Returns the time for the given value. |
Method | Return type | Description |
---|---|---|
Task.start(method:Method, from:Address, frequency:Number, mode:Enum, gasLimit:Number) | Task | Start the task by method name, from the given address and frequency(timestamp), the TaskMode and the gasLimit. |
Task.stop(task:Address) | None | Stop the task method. |
Task.current() | Task | Returns teh current task method. |
Method | Return type | Description |
---|---|---|
UID.generate() | Number | Returns a unique identifier. |
Method | Return type | Description |
---|---|---|
Map.get(key:Generic) | Generic | Returns the value by the given key. |
Map.set(key:Generic, value:Generic) | None | Set's the value to the specified key. |
Map.remove(key:Generic) | None | Removes the key from the map. |
Map.clear() | None | Clears all the Map entries. |
Map.count() | Number | Returns the number of entries in the Map. |
Map.has(key:Generic) | Bool | Returns if the key exists in the Map. |
Method | Return type | Description |
---|---|---|
List.get(index:Number) | Generic | Returns the element at the given index. |
List.add(value:Generic) | None | To add the value to the list. |
List.replace(index:Number, value:Generic) | None | Replaces the value at the given index. |
List.remove(index:Number) | None | Removes from the list at a given index. |
List.count() | Number | Returns the number of entries in the List. |
List.clear() | None | Clears all the entries in the List. |
Method | Return type | Description |
---|---|---|
String.toBytes(target:String) | Bytes | To convert a given string into a byte array, and returns it. |
String.length(target:String) | Number | Returns the length of the string. |
String.substr(target:String, index:Number, length:Number) | String | Returns the substring of the given string at an index and a length. |
String.toArray(target:String) | Array | To convert the String into an Array of Numbers that represent each Character in the String. |
String.fromArray(target:Array) | String | To convert a given Array into a String, each number represents a Character. |
Method | Return type | Description |
---|---|---|
Decimal.decimals(target:Any) | Number | Returns the number of decimals of the given target. |
Decimal.convert(decimalPlaces:Number, value:Number) | Number | Returns the converted value of the given decimal places. |
Method | Return type | Description |
---|---|---|
Enum.isSet(target:Enum<>, flag:Enum<>) | Bool | Returns true if the enum is set, false otherwise. |
Method | Return type | Description |
---|---|---|
Address.isNull(target:Address) | Bool | Returns true if the address is null, false otherwise. |
Address.isUser(target:Address) | Bool | Returns true if the address is a user, false otherwise. |
Address.isSystem(target:Address) | Bool | Returns true if the address is a System address, false otherwise. |
Address.isInterop(target:Address) | Bool | Returns true if the address is an Internal Operation, false otherwise. |
Method | Return type | Description |
---|---|---|
Module.getScript(target:Module) | Bytes | TODO |
Module.getABI(target:Module) | Bytes | TODO |
Method | Return type | Description |
---|---|---|
Format.decimals(value:Number, symbol:String) | String | Returns a string with the value formated to the given symbol. |
Format.symbol(symbol:String) | String | Returns a string representation of the given symbol. |
Format.account(address:Address) | String | Returns a String with the account formated to print. |
Macro | Description |
---|---|
$THIS_ADDRESS | The address of the current contract |
$THIS_SYMBOL | The symbol of the current token |
$TYPE_OF | The type of the argument |
Currently it is possible to throw exceptions with a string message.
Runtime.expect() can also be used as an alternative way of throwing exceptions based on a condition.
More work will include support for other data types and support for try..catch.
...
throw "something happened";
...
}
Previously you could always use the Call.xxx methods to interact with any deployed contract, but the Call.xxx methods don't have full type and argument validation. In latest version of TOMB, it is also possible to import any valid Phantasma ABI file. Using this feature of ABIfiles adds more compile time safety and is also easier to use.
For example, if we have a CustomContract that was previously compiled by TOMB, we should have at least a .pvm and a .abi file for it. That .abi file contains a list of public interface of the contract, and we can import that so that other contracts can call their methods.
...
import CustomContract; // by doing this, we can now call any public method of CustomContract
// the sintax is the same as other contract method calls, eg: CustomContract.doSomething(1234);
...
}
If you do a import CustomContract, the compiler will need to find a customcontract.abi file somewhere. By default, it will look in the output folder that you specified, but you can also pass extra library paths:
tomb.exe libpath:c:\something\compiled_abis target.tomb
You can also specify subfolders directly in the import.
...
import MyContracts.CustomContract; // this will search for a .abi file in a folder called MyContracts (within all possible libray path folders specified in the compiler args)
...
}
Simple contract that sums two numbers and returns the result
contract test {
public sum(a:number, b:number):number
{
return a + b;
}
}
Like most programming languages, it is possible to do conditional branching use if statement.
Logic operators supported include and, or and xor.
contract test {
public isEvenAndPositive(n:number):bool
{
local cond1:bool = n>0;
local cond2: bool = (n%2) == 0;
if (cond1 && cond2)
{
return true;
}
else
{
return false;
}
}
}
Simple contract that sums two numbers and returns the result
contract test {
public check(x:number): string {
switch (x) {
case 0: return "zero";
case 1: return "one";
case 2: return "two";
default: return "other";
}
}
}
const MY_CONST_X : number = 10; // constants can be declared globally (all scripts / contracts in file can use them)
contract test {
const MY_CONST_Y : number = 20; // constants can be declared inside a contract / script (only visible there)
public calculate(): number
{
return MY_CONST_X + MY_CONST_Y;
}
}
Simple contract that implements a global counter (that can be incremented by anyone who calls the contract).
Note that any global variable that is not generic must be initialized in the contract constructor.
contract test {
global counter: number;
constructor(owner:address)
{
counter = 0;
}
public increment()
{
if (counter < 0){
throw "invalid state";
}
counter += 1;
}
}
Another contract that implements a counter, this time unique per user address.
Showcases how to validate that a transaction was done by user possessing private keys to 'from' address
contract test {
import Runtime;
import Map;
global counters: storage_map<address, number>;
public increment(from:address)
{
Runtime.expect(Runtime.isWitness(from), "witness failed");
local temp: number;
temp = counters.get(from);
temp += 1;
counters.set(from, temp);
}
}
Simple contract that shows how to use strings and builtin type methods, string.length() in this specific case.
contract test {
global val: string;
constructor(owner:address)
{
val = "hello";
val += " world";
}
public getLength():number
{
return val.length();
}
}
There is compiler support for decimal numbers.
Note that internally those are converted to Number types in fixed point format.
contract test {
global val: decimal<4>; // the number between <> is the number of decimal places
constructor(owner:address)
{
val = 2.1425;
}
public getValue():number
{
return val; // this will return 21425, which is the previous value in fixed point format
}
public getDecimals():number
{
return val.decimals(); // this returns 4 as result
}
public sum(other:decimal<4>):number
{
return val + other;
}
}
It is now possible to use macros to automatically fetch the correct decimals.
token BOO {
property name: string = "BOO"; //placeHolder for compiler reasons
}
contract test {
global val: decimal<$BOO_DECIMALS>; // using a macro here instead of a hardcoded decimal
// all rest is similar
}
There is compiler support for enumerated value types, that map directly to the Phantasma VM enum type.
// NOTE - like other custom types, it is declared outside the scope of a contract
enum MyEnum { A = 0, B = 1, C = 2}
// if the numbers are sequential, it is ok to ommit them, eg:
//enum MyEnum { A, B, C}
contract test {
global state: MyEnum;
constructor(owner:address)
{
state = MyEnum.B;
}
public getValue():MyEnum
{
return state;
}
}
The compiler supports generic types, including maps.
Maps are one of the few types that don't have to initialized in the constructor.
contract test {
import Map;
global my_state: storage_map<address, number>;
constructor(owner:address)
{
my_state.set(owner, 42);
}
public getState(target:address):number
{
return my_state.get(target);
}
}
Here is an simple example of how to declare and initialize an array.
contract test {
import Array;
public getStrings(): array<string> {
local result:array<string> = {"A", "B", "C"};
return result;
}
}
The compiler supports casting strings into number arrays (unicode values) and number arrays back to strings.
contract test {
import Array;
public toUpper(s:string):string
{
local my_array: array<number>;
// extract chars from string into an array
my_array = s.toArray();
local length :number = Array.length(my_array);
local idx :number = 0;
while (idx < length) {
local ch : number = my_array[idx];
if (ch >= 97) {
if (ch <= 122) {
my_array[idx] = ch - 32;
}
}
idx += 1;
}
// convert the array back into a unicode string
local result:string = String.fromArray(my_array);
return result;
}
}
Every type provides methods to cast a variable to another compatible type.
NOTE: Many are missing in the method list in this documentation, but they come in the the form Type.toOtherType()
See examples below:
contract test {
import Time;
import Hash;
import String;
import Address;
public convertTimeToNumber(x:timestamp):number
{
return Time.toNumber(x);
}
public convertHashToString(x:hash):string
{
return Hash.toString(x);
}
public convertStringToNumber(x:string):number
{
return String.toNumber(x);
}
public convertAddressToString(x:address):string
{
return Address.toString(x);
}
}
It is possible to generate pseudo random numbers, and also to control the generation seed.
If a seed is not specified, then the current transaction hash will be used as seed, if available.
contract test {
import Random;
import Hash;
import Runtime;
global my_state: number;
public mutateState():number
{
// use the current transaction hash to provide a random seed. This makes the result deterministic during node consensus
// optionally we can use other value, depending on your needs, eg: Random.seed(16676869);
local tx_hash:hash = Runtime.transactionHash();
local mySeed:number = tx_hash.toNumber();
Random.seed(mySeed);
my_state = Random.generate() % 10; // Use modulus operator to constrain the random number to a specific range
return my_state;
}
}
A contract that takes a payment in tokens from a user.
Showcases how to transfer tokens and how to use macro $THIS_ADDRESS to obtain address of the contract.
contract test {
import Runtime;
import Token;
public paySomething(from:address, quantity:number)
{
Runtime.expect(Runtime.isWitness(from), "witness failed");
local price: number = 10;
price *= quantity;
local thisAddr:address = $THIS_ADDRESS;
Token.transfer(from, thisAddr, "SOUL", price);
// TODO after payment give something to 'from' address
}
}
There are also some builtin enums, like TokenFlags.
contract test {
import Runtime;
import Token;
public paySomething(from:address, amount:number, symbol:string,price:number)
{
Runtime.expect(Runtime.isWitness(from), "witness failed");
local flags:TokenFlags = Token.getFlags(symbol);
if (flags.isSet(TokenFlags.Fungible)) {
local thisAddr:address = $THIS_ADDRESS;
Token.transfer(from, thisAddr, "SOUL", price);
}
}
}
Runtime.Expect offers a clean way to validate conditions.
The developer of a smart contract must be very careful to ensure that no exploits are possible due to missing validations.
contract test {
import Runtime;
public doSomething(from:address) {
Runtime.expect(from.isUser(), "expected user address"); // makes sure the address is of 'user' type
Runtime.expect(Runtime.isWitness(from), "invalid witness"); // makes sure the transaction was signed by 'from' address
Runtime.expect(Runtime.gasTarget() == $THIS_ADDRESS, "invalid donation"); // makes sure the transaction fees are donated to this contract
// actually do something after passing all validation
}
}
Showcases how a contract method can call other methods.
If a method is declared private, it can't be called by anyone except the actual contract that implements it.
contract test {
import Call; //alternative method to call another method
private sum(a:number, b:number): number {
return a + b;
}
public calculatePrice(x:number): number
{
local price: number = 10;
price = this.sum(price, x); // here we use 'this' for calling another method
//price = Call.method<number>(sum,price,x); //alternative way to call the method
return price;
}
}
A script is something that can be used either for a transaction or for an API invokeScript call.
This example showcases a simple script with one argument, that calls a contract.
Note that for scripts with arguments, for them to run properly you will have to push them into the stack before.
script startup {
import Call;
code(target:address) {
local temp:number = 50000;
Call.contract("Stake", "Unstake", target, temp);
}
}
This example showcases a script that deploys a token contract.
token GHOST {
property name: string = "GHOST"; //placeHolder for compiler reasons
}
script deploy {
import Token;
import Module;
code() {
local maxSupply:number = 50000;
local decimals:number = 1;
local flags:TokenFlags = TokenFlags.None;
Token.create(@P2KAkQRrL62zYvb5198CHBLiKHKr4bJvAG7aXwV69rtbeSz, "GHOST", "Ghost Token", maxSupply, decimals, flags, Module.getScript(GHOST), Module.getABI(GHOST));
}
}
This example showcases a script that mints an custom NFT.
struct my_rom_data {
name:string;
counter:number;
}
script token_minter {
import Token;
import NFT;
code(source:address, target:address) {
local rom_data:my_rom_data = Struct.my_rom_data("hello", 123);
NFT.mint(source, target, "LOL", rom_data, "ram_can_be_anything",0);
}
}
Inline asm allows to write assembly code that is then inserted and merged into the rest of the code.
This feature is useful as an workaround for missing features in the compiler.
script startup {
code() {
local temp: string ="";
asm {
LOAD $temp "hello"
}
}
}
Showcases how a contract can declare and emit custom events.
contract test {
import Token;
import Runtime;
event MyPayment:number = "{address} paid {data}"; // here we use a short-form description
public paySomething(from:address, x:number)
{
Runtime.expect(Runtime.isWitness(from), "witness failed");
local price: number = 10;
local thisAddr:address = $THIS_ADDRESS;
Token.transfer(from, thisAddr, "SOUL", price);
emit MyPayment(from, price);
}
}
A more complex version of the previous example, showcasing custom description scripts.
description payment_event {
code(from:address, amount:number): string {
local result:string = "";
result += from;
result += " paid ";
result += amount;
return result;
}
}
contract test {
event MyPayment:number = payment_event; // here we use a short-form declaration
// everything else would be same as previous example
}
A yet more complex version of the previous examples, showcasing custom description scripts and also struct declarations.
struct my_event_data {
amount:number;
symbol:string;
}
description payment_event {
code(from:address, data:my_event_data): string {
local result:string = "";
result += from;
result += " paid ";
result += data.amount;
result += " ";
result += data.symbol;
return result;
}
}
contract test {
event MyPayment:my_event_data = payment_event; // here we use a short-form declaration
// everything else would be same as previous examples
}
A contract example showcasing triggers.
In this example, this account will only accept transfers of KCAL and reject anything else.
contract test {
trigger onReceive(from:address,to:address,symbol:string, amount:number)
{
if (symbol != "KCAL") {
throw "can't receive asset: " + symbol;
}
return;
}
}
Another contract example showcasing triggers.
In this example, any asset sent to this account will be auto-converted into SOUL.
contract test {
import Call;
trigger onReceive(from:address,to:address, symbol:string, amount:number)
{
if (symbol != "SOUL") {
Call.contract<none>("Swap", "SwapTokens", from, symbol, "SOUL", amount);
}
return;
}
}
It is possible to use methods as variables.
contract test {
import Runtime;
import Map;
import Call;
global _counter:number;
global _callMap: storage_map<address,method>;
constructor(owner:address)
{
_counter = 0;
}
private incCounter(target:address) {
_counter += 1;
}
private subCounter(target:address) {
_counter -= 1;
}
public registerUser(from:address, amount:number)
{
local target: method;
if (amount > 10) {
target = Call.method<method>(incCounter,from);
}
else {
target = Call.method<method>(subCounter,from);
}
_callMap.set(from, target);
}
}
It is possible in TOMB to return multiple results from a single method.
The method return type must be marked with an asterisk, then multiple returns can be issued.
A return without expression will terminate the method execution.
contract test {
// this method returns an array of strings (could also be numbers, structs, etc)
public getStrings(): string* {
return "hello";
return "world";
return;
}
}
It is possible to let TOMB compiler auto-detect type of a local variable if you omit the type and provide an initialization expression.
contract test {
public calculate():string {
local a = "hello ";
local b = "world";
return a + b;
}
}
A task allows a contract method to run periodically without user intervention.
Tasks can't have parameters, however you can use Task.current() along with a global Map to associate custom user data to each task.
contract test {
import Time;
import Task;
global victory:bool;
global deadline:timestamp;
constructor(owner:address) {
victory = false;
time = Time.now() + time.hours(2);
Task.start(checkResult, owner, 0, TaskFrequency.Always, 999);
}
task checkResult() {
if (victory) {
break;
}
local now: timestamp = Time.now();
if (time >= deadline) {
break;
}
continue;
}
public win(from:address)
{
victory = true;
}
}
Yet another contract example showcasing triggers.
In this example, a multi-signature account is implemented.
contract test {
import Runtime;
import List;
import Call;
global owners: storage_list<address>;
constructor(owner:address)
{
owners.add(owner);
}
private validateSignatures() {
local index:number = 0;
local count:number = owners.count();
while (index < count) {
local addr:address = owners.get(index);
if (!Runtime.isWitness(addr)) {
throw "missing signature of "+addr;
}
}
}
public isOwner(target:address):bool {
local index:number = 0;
local count:number = owners.count();
while (index < count) {
local addr:address = owners.get(index);
if (addr == target) {
return true;
}
}
return false;
}
public addOwner(target:address) {
Runtime.expect(!this.isOwner(target), "already is owner");
this.validateSignatures();
owners.add(target);
}
trigger onSend(from:address, to:address, symbol:string,amount:number){
this.validateSignatures();
}
}
Showcases how to implement a fungible token (eg: the Phantasma equivalent to an Ethereum ERC20).
token DOG { // this defines the token symbol as DOG
import Runtime;
property name:string = "Dog Token";
property isFungible: bool = true;
property isDivisible: bool = true;
property decimals:number = 8; // required only if isDivisible is true
property isTransferable: bool = true;
property isBurnable: bool = true;
property isFinite: bool = false;
//property maxSupply: number = 1000000; // required only if isFinite is true
global _admin: address;
constructor(owner:address) {
_admin = owner;
}
// allows the token to be upgraded later, remove this trigger if you want a imutable fungible token
trigger onUpgrade(from:address)
{
Runtime.expect(Runtime.isWitness(_admin), "witness failed");
return;
}
// its possible to also add more triggers, custom methods etc
}
Showcases how to implement an NFT, showcasing all details including ROM, RAM and token series.
When creating an NFT, they must have 4 default properties implemented and they're:
name
, returns the name of the NFTdescription
, returns the descriptions of the NFTimageURL
, returns the image URL of the NFTinfoURL
, returns the info URL of the NFT
struct luchador_rom
{
genes:bytes;
name:string;
}
struct luchador_ram
{
experience:number;
level:number;
}
struct item_rom
{
kind:number;
quality:number;
}
struct item_ram
{
power:number;
level:number;
}
token NACHO {
import NFT;
global _owner: address;
const LUCHADOR_SERIES: number = 1;
const LUCHADOR_SUPPLY: number = 100000;
const ITEM_SERIES: number = 2;
const ITEM_SUPPLY: number = 500000;
property name: string = "Nachomen";
property isBurnable: bool = true;
property isFinite: bool = true;
property isFungible: bool = false;
property maxSupply: number = LUCHADOR_SUPPLY + ITEM_SUPPLY;
nft luchador<luchador_rom, luchador_ram> {
property name: string {
return _ROM.name;
}
property description: string {
return "Luchador with level " + _RAM.level;
}
property imageURL: string {
return "https://nacho.men/img/luchador/"+ _tokenID;
}
property infoURL: string {
return "https://nacho.men/api/luchador/"+ _tokenID;
}
}
nft item<item_rom, item_ram> {
property name: string {
local rom_kind: number = _ROM.kind;
switch(rom_kind)
{
case 1: return "Potion";
case 2: return "Gloves";
default: return "Item #" + _ROM.kind;
}
}
property description: string {
return "Item level " + _RAM.level;
}
property imageURL: string {
return "https://nacho.men/img/item/"+ _tokenID;
}
property infoURL: string {
return "https://nacho.men/api/item/"+ _tokenID;
}
}
constructor (addr:address) {
_owner = addr;
// at least one token series must exist, here we create 2 series
// they don't have to be created in the constructor though, can be created later
NFT.createSeries(_owner, $THIS_SYMBOL, LUCHADOR_SERIES, LUCHADOR_SUPPLY, TokenSeries.Unique, luchador);
NFT.createSeries(_owner, $THIS_SYMBOL, ITEM_SERIES, ITEM_SUPPLY, TokenSeries.Unique, item);
}
}
Many times it's necessary to obtain a list of NFTs owned by a specific address. For that you can use the NFT.getOwnerships() method. Note that this method requires a symbol. If you need to know all the owned NFTs of a specific address, call NFT.availableSymbols and iterate over it.
contract test {
import NFT;
import Array;
public getOwnedList(from:address) : array<number> {
local symbol: string = "MYTOK";
local result: array<number> = NFT.getOwnerships(from, symbol);
return result;
}
}
Many times it's necessary to obtain a list of NFTs infused into a specific NFT. For that you can use the NFT.getInfusions() method. It will return an array of Infusion structs, that have a Symbol: string and Value: Number as fields. Note that the Value field will be an amount for fungible-tokens and a tokenID for NFTs.
contract test {
import NFT;
import Array;
public getFirstInfusedToken(infusedTokenID: number) : Infusion {
local nfts: array<Infusion> = NFT.getInfusions("MYTOK", infusedTokenID);
local first: Infusion = nfts[0];
return first;
}
}
Besides the macros listed in the Available macros section, each of your contracts will also come with its own macros.
Contract macros are useful when you have multiple contracts in the same file.
Note that for things not provided via macros you can also use global constants.
token BOO {
property name: string = "BOO"; //placeHolder for compiler reasons
}
contract simple_contract {
constructor (addr:address) {
// ...
}
}
contract my_test {
import Runtime;
import Token;
public transferBoos(quantity:number)
{
// The line below showcases 3 different compiler macros.
// $SIMPLE_CONTRACT_ADDRESS => This macro is the address of the simple_contract declared previously.
// $THIS_ADDRESS => This macro is the address of the contract currently executing (my_test contract).
// $BOO_SYMBOL => This macro is the symbol of the BOO token (the string "BOO")
Token.transfer($SIMPLE_CONTRACT_ADDRESS, $THIS_ADDRESS, $BOO_SYMBOL, quantity);
}
}
Example showcasing calling contract storage directly from another contract.
Basic version where another storage map is called, and this value is incremented and stored in another contract storage.
contract test {
import Map;
import Storage;
import Call;
global counters: storage_map<number, number>;
private getContractCount(tokenId:number):number {
local count:number = Call.interop<none>("Map.Get", "OTHERCONTRACT", "_storageMap", tokenId, $TYPE_OF(number));
return count;
}
public updateCount(tokenID:number) {
local contractCounter:number = this.getContractCount(tokenID);
contractCounter += 1;
counters.set(tokenID, contractCounter);
}
public getCount(tokenID:number):number {
local temp:number = counters.get(tokenID);
return temp;
}
}
It is possible to declare a variable that will be bound to a register, with the value being kept between a public contract call and all internal private calls. The value of register is not persisted anywhere (either do it manually or use globals instead). This is an advanced use case, it will bypass several compile time checks and can break the logic of your contract if not used properly.
NOTE - This is broken due to frame allocation during CALL opcode
contract test {
register myReg : number;
private mutate():number
{
myReg = myReg + 1;
}
public fetch():number
{
myReg = 1;
this.mutate();
return myReg;
}
}
TOMB currently contains several "builtin" methods, aka code written in TOMB language itself and available as library for other contracts to use. When compiling code that uses the builtins, the builtin asm is concatened to your contract asm and assembled together, allowing everything to work seamlessy. This is a way to share useful code that is used in many contracts but not part of the Phantasma chain native methods.
In order to create new more builtin methods, do the following steps:
- Write code for a new method, making sure the method name follows the following convention: tomb_X_Y(args), where X is the library name to insert the builtin and Y is the method "real" name that will be exposed in the library
- Add that code to the builtins source code
- Compile builtins.tomb using TOMB itself.
- Open the generated builtins.asm file and copy paste the content into the BUILTIN_ASM static string inside Builtins.cs
- Your new method can now be used in any TOMB contract. You can do this change locally in your compiler repo and any contract compiled with it will still work anywhere.
The current support for Solidity is experimental, and some features of the language are still not supported. Regarding Phantasma specific features, it's possible to use Solidity import keyword to import any of the Phantasma features (eg: Runtime, Token, etc).
The following table lists a list of Solidity features and how it maps to Phantasma features
Solidity Feature | Phantasma equivalent | Notes |
---|---|---|
mapping(x => y) | storage_map<x,y> | Fully working |
uint8, int256, etc | number | Unsigned types are not supported yet by the compiler (will default to signed) |
string public constant name = "hello"; | property name:string = "hello"; | Fully working |
import "Phantasma/Runtime.tomb"; | import Runtime; | Fully working |
function something(uint x) public view returns (uint) | function something(x:number): number; | Fully working |
constructor(address owner) public | constructor(owner:address) | Constructors in Solidity can have multiple args, but in Phantasma only a single address |
event Something(uint x) | event Something:number = "{x} happened" | Not implemented yet |
Check our official Phantasma documentation for more info about developing with Phantasma Chain.