Programming Hotmoka
A tutorial on Hotmoka and smart contracts in Takamaka
3.4 Calling a method on an object in a Hotmoka node
(See the io-hotmoka-tutorial-examples-family-exported and the io-hotmoka-tutorial-examples-runs projects in https://github.com/Hotmoka/hotmoka)
In the previous section, we have created an object of class Person in the store of the node. Let us invoke the toString() method on that object now. For that, we can use the moka objects call command, specifying our ‘Person‘ object as receiver.
moka objects call afed291bbba43215c3d44124b2c46e06a803422edd55cf620811bc843c28baac#0 io.hotmoka.tutorial.examples.family.Person toString --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=7 e320d60d2d174e3edd4d1fd93e169cd1a878250803ccd729ec4709fb53ea0d0#0
Adding transaction e2fba03faf6ef49ade23767231cd778516e6a88e942d66f0ea231d0da90d788f... rejected. The transaction failed with message Class io.hotmoka.tutorial.examples.family.Person of the parameter 7e320d60d2d174e3edd4d1fd93e169cd1a878250803ccd729ec4709fb53ea0d0#0 is not exported: add @Exported to io.hotmoka.tutorial.examples.family .Person
Command moka objects call requires to specify, as its first arguments, the payer of the call, that is, our account, followed by the name of the class whose method is called and the name of that method. The receiver of the call is specified through --receiver.
As you can see above, the result is deceiving.
This exception occurs when we try to pass the Person object as receiver of toString() (the receiver is a particular case of an actual argument). That object has been created in store, has escaped the node and is available through its storage reference 7e320d60d2d174e3…#0. However, it cannot be passed back into the node as argument of a call since it is not exported. This is a security feature of Hotmoka. Its reason is that the store of a node is public and can be read freely. Everybody can see the objects created in the store of a Hotmoka node and their storage references can be used to invoke their methods and modify their state. This is true also for objects meant to be private state components of other objects and that are not expected to be freely modifiable from outside the node. Because of this, Hotmoka requires that classes, whose instances can be passed into the node as arguments to methods or constructors, must be annotated as @Exported. This means that the programmer acknowledges the use of these instances from outside the node.
Note that all objects can be passed, from inside the blockchain, as arguments to methods of code in the node. The above limitation applies to objects passed from outside the node only.
Let us modify the Person class again:
...
import io.takamaka.code.lang.Exported;
...
@Exported
public class Person extends Storage {
...
}
Package the project io-hotmoka-tutorial-examples-family:
cd io-takamaka-code-examples-family mvn clean install cd .. moka jars install afed291bbba43215c3d44124b2c46e06a803422edd55cf620811bc843c28baac#0 io-hotmoka-tutorial-examples-family/target/io-hotmoka-tutorial-examples-family-1.12.2.jar --yes --password-of-payer --uri=ws://panarea.hotmoka.io:8001
Enter value for --password-of-payer (the password of the key pair of the payer account): chocolate The jar has been installed at 90c13304b1444a3ff29d25162b5bbbc932ca5cca04e5826a5ae07d756460fdc2. Gas consumption: * total: 497432 * for CPU: 15369 * for RAM: 5183 * for storage: 476880 * for penalty: 0 * price per unit: 1 pana * total price: 497432 panas
then create a new Person object:
moka objects create afed291bbba43215c3d44124b2c46e06a803422edd55cf620811bc843c28baac#0 io.hotmoka.tutorial.examples.family.Person Einstein 14 4 1879 null null --classpath=90c13304b1444a3ff29d25162b5bbbc932ca5cca04e5826a5ae07d756460fdc2 -- password-of-payer --uri=ws://panarea.hotmoka.io:8001
Enter value for --password-of-payer (the password of the key pair of the payer account): chocolate Do you really want to call constructor public ...Person(java.lang.String,int,int,int,...Person,...Person) spending up to 1000000 gas units at the price of 1 pana per unit (that is, up to 1000000 panas) [Y/N] Y A new object 595478b5c62775cf687450ffb3ca13d86339aa06d7c0faba21d7026df02cbe9e#0 has been created. Gas consumption: * total: 58708 * for CPU: 14951 * for RAM: 5037 * for storage: 38720 * for penalty: 0 * price per unit: 1 pana * total price: 58708 panas
and finally call toString() on that new object:
moka objects call afed291bbba43215c3d44124b2c46e06a803422edd55cf620811bc843c28baac#0 io.hotmoka.tutorial.examples.family.Person toString --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=595478 b5c62775cf687450ffb3ca13d86339aa06d7c0faba21d7026df02cbe9e#0
Adding transaction 784ab94a16de6abff55303e0b4921fe634a6ecf44613350912b58d829539738c... done. The method returned: Einstein (14/4/1879) Gas consumption: * total: 48817 * for CPU: 15948 * for RAM: 7109 * for storage: 25760 * for penalty: 0 * price per unit: 1 pana * total price: 48817 panas
This time, the correct answer Einstein (14/4/1879) appears on the screen.
In Ethereum, the only objects that can be passed, from outside the blockchain, as argument to method calls into blockchain are contracts. Namely, in Solidity it is possible to pass such objects as their untyped address that can only be cast to contract classes. Takamaka allows more, since any object can be passed as argument, not only contracts, as long as its class is annotated as @Exported. This includes all contracts since the class io.takamaka.code.lang.Contract, that we will present later, is annotated as @Exported and @Exported is an inherited Java annotation.
We can do the same in code, instead of using the moka objects call command. Namely, we can expand the FamilyStorage class seen before in order to run a further transaction, that calls toString(). For that, copy then the following FamilyExported class inside the package io.hotmoka.tutorial.examples.runs of the io-hotmoka-tutorial-examples-runs project:
package io.hotmoka.tutorial.examples.runs;
import static io.hotmoka.helpers.Coin.panarea;
import static io.hotmoka.node.StorageTypes.INT;
import static java.math.BigInteger.ONE;
import java.math.BigInteger;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import io.hotmoka.constants.Constants;
import io.hotmoka.crypto.api.Signer;
import io.hotmoka.helpers.GasHelpers;
import io.hotmoka.helpers.SignatureHelpers;
import io.hotmoka.node.Accounts;
import io.hotmoka.node.ConstructorSignatures;
import io.hotmoka.node.MethodSignatures;
import io.hotmoka.node.StorageTypes;
import io.hotmoka.node.StorageValues;
import io.hotmoka.node.TransactionRequests;
import io.hotmoka.node.api.Node;
import io.hotmoka.node.api.requests.SignedTransactionRequest;
import io.hotmoka.node.api.transactions.TransactionReference;
import io.hotmoka.node.api.types.ClassType;
import io.hotmoka.node.api.values.StorageReference;
import io.hotmoka.node.api.values.StorageValue;
import io.hotmoka.node.remote.RemoteNodes;
public class FamilyExported {
private final static ClassType PERSON = StorageTypes.classNamed
("io.hotmoka.tutorial.examples.family.Person");
public static void main(String[] args) throws Exception {
// the path of the user jar to install
var familyPath = Paths.get(System.getProperty("user.home")
+ "/.m2/repository/io/hotmoka/io-hotmoka-tutorial-examples-family/"
+ Constants.HOTMOKA_VERSION
+ "/io-hotmoka-tutorial-examples-family-" + Constants.HOTMOKA_VERSION + ".jar");
var dir = Paths.get(args[1]);
var payer = StorageValues.reference(args[2]);
var password = args[3];
try (var node = RemoteNodes.of(new URI(args[0]), 150000)) {
// we get a reference to where io-takamaka-code-X.Y.Z.jar has been stored
TransactionReference takamakaCode = node.getTakamakaCode();
// we get the signing algorithm to use for requests
var signature = node.getConfig().getSignatureForRequests();
KeyPair keys = loadKeys(node, dir, payer, password);
// we create a signer that signs with the private key of our account
Signer<SignedTransactionRequest<?>> signer = signature.getSigner
(keys.getPrivate(), SignedTransactionRequest::toByteArrayWithoutSignature);
// we get the nonce of our account: we use the account itself as caller and
// an arbitrary nonce (ZERO in the code) since we are running
// a @View method of the account
BigInteger nonce = node
.runInstanceMethodCallTransaction(TransactionRequests.instanceViewMethodCall
(payer, // payer
BigInteger.valueOf(100_000), // gas limit
takamakaCode, // class path for the execution of the transaction
MethodSignatures.NONCE, // method
payer)).get() // receiver of the method call
.asBigInteger(__ -> new ClassCastException());
// we get the chain identifier of the network
String chainId = node.getConfig().getChainId();
var gasHelper = GasHelpers.of(node);
// we install the family jar in the node: our account will pay
TransactionReference family = node
.addJarStoreTransaction(TransactionRequests.jarStore
(signer, // an object that signs with the payer's private key
payer, // payer
nonce, // payer's nonce: relevant since this is not a call to a @View method!
chainId, // chain identifier: relevant since this is not a call to a @View method!
BigInteger.valueOf(1_000_000), // gas limit: enough for this small jar
gasHelper.getSafeGasPrice(), // gas price: at least the current gas price of the network
takamakaCode, // class path for the execution of the transaction
Files.readAllBytes(familyPath), // bytes of the jar to install
takamakaCode)); // dependencies of the jar that is being installed
// we increase our copy of the nonce, ready for further
// transactions having the account as payer
nonce = nonce.add(ONE);
// call the constructor of Person and store in einstein the new object in blockchain
StorageReference einstein = node.addConstructorCallTransaction
(TransactionRequests.constructorCall
(signer, // an object that signs with the payer's private key
payer, // payer
nonce, // payer's nonce: relevant since this is not a call to a @View method!
chainId, // chain identifier: relevant since this is not a call to a @View method!
BigInteger.valueOf(300_000), // gas limit: enough for a small object
panarea(gasHelper.getSafeGasPrice()), // gas price, in panareas
family, // class path for the execution of the transaction
// constructor Person(String,int,int,int)
ConstructorSignatures.of(PERSON, StorageTypes.STRING, INT, INT, INT),
// actual arguments
StorageValues.stringOf("Einstein"), StorageValues.intOf(14),
StorageValues.intOf(4), StorageValues.intOf(1879)
));
// we increase our copy of the nonce, ready for further
// transactions having the account as payer
nonce = nonce.add(ONE);
StorageValue s = node.addInstanceMethodCallTransaction
(TransactionRequests.instanceMethodCall
(signer, // an object that signs with the payer's private key
payer, // payer
nonce, // payer's nonce: relevant since this is not a call to a @View method!
chainId, // chain identifier: relevant since this is not a call to a @View method!
BigInteger.valueOf(100_000), // gas limit: enough for a simple call
panarea(gasHelper.getSafeGasPrice()), // gas price, in panareas
family, // class path for the execution of the transaction
// method to call: String Person.toString()
MethodSignatures.ofNonVoid(PERSON, "toString", StorageTypes.STRING),
// receiver of the method to call
einstein
)).get();
// we increase our copy of the nonce, ready for further
// transactions having the account as payer
nonce = nonce.add(ONE);
// print the result of the call
System.out.println(s);
}
}
private static KeyPair loadKeys(Node node, Path dir, StorageReference account, String password) throws Exception {
return Accounts.of(account, dir)
.keys(password, SignatureHelpers.of(node).signatureAlgorithmFor(account));
}
}
The interesting part is the call to addInstanceMethodCallTransaction() at the end of the previous listing. It requires to resolve method Person.toString() by using einstein as receiver (the type StorageTypes.STRING is the return type of the method) and to run the resolved method. It stores the result in s, that subsequently prints on the standard output. You can run class FamilyExported. You will obtain the same result as with moka objects call:
mvn compile exec:java -Dexec.mainClass="io.hotmoka.tutorial.examples.runs.FamilyExported" -Dexec.args="ws://panarea.hotmoka.io:8001 hotmoka_tutorial afed291bbba43215c3d44124b2c46e06a803422edd55cf620811bc843c28baac#0 chocolate"
Einstein (14/4/1879)
As we have shown, method addInstanceMethodCallTransaction() can be used to invoke an instance method on an object in the store of the node. This requires some clarification. First of all, note that the signature of the method to call is resolved and the resolved method is then invoked. If such resolved method is not found (for instance, if we tried to call tostring() instead of toString()), then addInstanceMethodCallTransaction() would end up in a failed transaction. Moreover, the usual resolution mechanism of Java methods applies. If, for instance, we invoked MethodSignatures.ofNonVoid(StorageTypes.OBJECT, "toString", StorageTypes.STRING) instead of MethodSignatures.ofNonVoid(PERSON, "toString", StorageTypes.STRING), then method toString() would have be resolved from the run-time class of einstein, looking for the most specific implementation of toString(), up to the java.lang.Object class, which would anyway end up in running Person.toString().
Method addInstanceMethodCallTransaction() can be used to invoke instance methods with parameters. If a toString(int) method existed in class Person, then we could call it and pass 2019 as its argument, by writing:
StorageValue s = node.addInstanceMethodCallTransaction (TransactionRequests.instanceMethodCall( ... // method to call: String Person.toString(int) MethodSignatures.ofNonVoid(PERSON, "toString", StorageTypes.STRING, StorageTypes.INT), // receiver of the method to call einstein, // actual argument(s) StorageValues.intOf(2019) )).get();
where we have added the formal parameter StorageTypes.INT and the corresponding actual argument StorageValues.intOf(2019). Note that method calls yields an optional value in Hotmoka, since methods returning void actually return non value. Consequently, we need the final call to get() above to get the actual value returned by the method.
Method addInstanceMethodCallTransaction() cannot be used to call a static method. For that, use addStaticMethodCallTransaction() instead, that accepts a request similar to that for addInstanceMethodCallTransaction(), but without a receiver.

In object-oriented languages, the receiver of a call to a non-static method is the object over which the method is executed, that is accessible as this inside the code of the method. In our case, we want to invoke einstein.toString(), where einstein is the object that we have created previously, hence the receiver of the call. The receiver can be seen as an implicit actual argument passed to a (non-static) method.