(image)

Programming Hotmoka
A tutorial on Hotmoka and smart contracts in Takamaka

5.3 Storage maps

Maps are dynamic associations of objects to objects. They are useful for programming smart contracts, as their extensive use in Solidity proves. However, most such uses are related to the withdrawal pattern, that is not needed in Takamaka. Nevertheless, there are still situations when maps are useful in Takamaka code, as we show below.

Java has many implementations of maps. However, they are not storage objects and consequently cannot be stored in a Hotmoka node. This section describes the Takamaka library classes io.takamaka.code.util.StorageTreeMap and SnapshottableStorageTreeMap, that extend Storage and whose instances can then be held in the store of a node, if keys K and values V can be stored in a node as well.

(-tikz- diagram)

Figure 5.6: The hierarchy of storage maps.

We refer to the JavaDoc of StorageTreeMap and SnapshottableStorageTreeMap for a full description of their methods, that are similar to those of traditional Java maps. Here, we just observe that a key is mapped into a value by calling method void put(K key, V value), while the value bound to a key is retrieved by calling V get(Object key). It is possible to yield a default value when a key is not in the map, by calling V getOrDefault(Object key, V _default) or its sibling V getOrDefault(Object key, Supplier _default), that evaluates the default value only if needed. Method V putIfAbsent(K key, V value) binds the key to the value only if the key is unbound. Similarly for its sibling V computeIfAbsent(K key, Function value) that computes the new value only if needed (these two methods differ for their returned value, as in Java maps. Please refer to their JavaDoc).

Instances of StorageTreeMap and SnapshottableStorageTreeMap keep keys in increasing order. Namely, if type K has a natural order, that order is used. Otherwise, keys (that must be storage objects) are kept ordered by increasing storage reference. Consequently, methods forEach(Consumer> action), forEachKey(Consumer action) and forEachValue(Consumer action) perform an internal iteration of the elements of the map, in order.

Compare this with Solidity, where maps do not know the set of their keys nor the set of their values, so that, in Solidity, it is impossible to iterate on maps.

Fig. 5.6 shows the hierarchy of the storage map classes. They implement the library interface StorageMap, that defines the methods that modify a map. That interface extends the interface StorageMapView that, instead, defines the methods that read data from a map, but do not modify it. Methods snapshot() and view() return an @Exported StorageMapView, in constant time. Only instances of class SnapshottableStorageTreeMap allow the creation of snapshots. Because of that, they are slightly more expensive, in time, space and gas, than instances of class StorageTreeMap. Therefore, use always a StorageMapView whenever snapshots are not needed.

There are also specialized map classes, optimized for specific primitive types of keys, such as StorageTreeIntMap, whose keys are int values. We refer to their JavaDoc for further information.

Next section shows an example of use for StorageTreeMap.

5.3.1 A blind auction contract

(See the io-hotmoka-tutorial-examples-auction project in https://github.com/Hotmoka/hotmoka)

This section exemplifies the use of class StorageTreeMap by writing a smart contract that implements a blind auction. That contract allows a beneficiary to sell an item to the buying contract that offers the highest bid. Since data in blockchain is public, in a non-blind auction it is possible that bidders eavesdrop the offers of other bidders in order to place an offer that is only slightly higher than the current best offer. A blind auction, instead, uses a two-phases mechanism: in the initial bidding time, bidders place bids, hashed, so that they do not reveal their amount. After the bidding time expires, the second phase, called reveal time, allows bidders to reveal the real values of their bids and the auction contract to determine the actual winner. This works since, to reveal a bid, each bidder provides the real data of the bid. The auction contract then recomputes the hash from real data and checks if the result matches the hash provided at bidding time. If not, the bid is considered invalid. Bidders can even place fake offers on purpose, in order to confuse other bidders.

Create in Eclipse a new Maven Java 21 (or later) project. Use for this project the name io-hotmoka-tutorial-examples-auction. You could do this for instance by duplicating the project io-hotmoka-tutorial-examples-family. Use the following pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">


  <modelVersion>4.0.0</modelVersion>
  <groupId>io.hotmoka</groupId>
  <artifactId>io-hotmoka-tutorial-examples-auction</artifactId>
  <version>|\hotmokaVersion{}|</version>


  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>21</maven.compiler.release>
  </properties>


  <dependencies>
    <dependency>
      <groupId>io.hotmoka</groupId>
      <artifactId>io-takamaka-code</artifactId>
      <version>|\takamakaVersion{}|</version>
    </dependency>
  </dependencies>


  <build>
    <plugins>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
         <version>3.11.0</version>
      </plugin>
    </plugins>
  </build>


</project>

and the following module-info.java:

module auction {
    requires io.takamaka.code;
}

Create package io.hotmoka.tutorial.examples.auction inside src/main/java and add the following BlindAuction.java inside that package. It is a Takamaka contract that implements a blind auction. Since each bidder may place more bids and since such bids must be kept in storage until reveal time, this code uses a map from bidders to lists of bids. This smart contract has been inspired by a similar Ethereum contract in Solidity available at https://docs.soliditylang.org/en/v0.8.33/solidity-by-example.html#blind-auction. Please note that the code below does not compile yet, since it misses two classes that we will define in the next section.

package io.hotmoka.tutorial.examples.auction;


import static io.takamaka.code.lang.Takamaka.event;
import static io.takamaka.code.lang.Takamaka.now;
import static io.takamaka.code.lang.Takamaka.require;


import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.util.function.Supplier;


import io.takamaka.code.lang.Contract;
import io.takamaka.code.lang.Exported;
import io.takamaka.code.lang.FromContract;
import io.takamaka.code.lang.Payable;
import io.takamaka.code.lang.PayableContract;
import io.takamaka.code.lang.Storage;
import io.takamaka.code.lang.StringSupport;
import io.takamaka.code.math.BigIntegerSupport;
import io.takamaka.code.security.SHA256Digest;
import io.takamaka.code.util.Bytes32Snapshot;
import io.takamaka.code.util.StorageLinkedList;
import io.takamaka.code.util.StorageList;
import io.takamaka.code.util.StorageMap;
import io.takamaka.code.util.StorageTreeMap;


/**
* A contract for a simple auction. This class is derived from the Solidity
* code shown at https://docs.soliditylang.org/en/v0.8.33/
* solidity-by-example.html#blind-auction
* In this contract, bidders place bids together with a hash. At the end of
* the bidding period, bidders are expected to reveal if and which of their
* bids were real and their actual value. Fake bids are refunded. Real bids
* are compared and the bidder with the highest bid wins.
*/
public class BlindAuction extends Contract {


     /**
     * A bid placed by a bidder. The deposit has been payed in full.
     * If, later, the bid will be revealed as fake, then the deposit will
     * be fully refunded. If, instead, the bid will be revealed as real, but
     * for a lower amount, then only the difference will be refunded.
     */
     private static class Bid extends Storage {


          /**
          * The hash that will be regenerated and compared at reveal time.
          */
          private final Bytes32Snapshot hash;


          /**
          * The value of the bid. Its real value might be lower and known
          * at real time only.
          */
          private final BigInteger deposit;


          private Bid(Bytes32Snapshot hash, BigInteger deposit) {
               this.hash = hash;
               this.deposit = deposit;
          }


          /**
          * Recomputes the hash of a bid at reveal time and compares it
          * against the hash provided at bidding time. If they match,
          * we can reasonably trust the bid.
          *
          * @param revealed the revealed bid
          * @param digest the hasher
          * @return true if and only if the hashes match
          */
          private boolean matches(RevealedBid revealed, SHA256Digest digest) {
               digest.update(BigIntegerSupport.toByteArray(revealed.value));
               digest.update(revealed.fake ? (byte) 0 : (byte) 1);
               digest.update(revealed.salt.toArray());
               byte[] arr1 = hash.toArray();
               byte[] arr2 = digest.digest();


               if (arr1.length != arr2.length)
                 return false;


               for (int pos = 0; pos <arr1.length; pos++)
                 if (arr1[pos] != arr2[pos])
                   return false;


               return true;
          }
     }


     /**
     * A bid revealed by a bidder at reveal time. The bidder shows
     * if the corresponding bid was fake or real, and how much was the
     * actual value of the bid. This might be lower than previously
     * communicated.
     */
     @Exported
     public static class RevealedBid extends Storage {
          private final BigInteger value;
          private final boolean fake;


          /**
          * The salt used to strengthen the hashing.
          */
          private final Bytes32Snapshot salt;


          public RevealedBid(BigInteger value, boolean fake, Bytes32Snapshot salt) {
               this.value = value;
               this.fake = fake;
               this.salt = salt;
          }
     }


     /**
     * The beneficiary that, at the end of the reveal time, will receive
     * the highest bid.
     */
     private final PayableContract beneficiary;


     /**
     * The bids for each bidder. A bidder might place more bids.
     */
     private final StorageMap<PayableContract, StorageList<Bid>> bids = new StorageTreeMap<>();


     /**
     * The time when the bidding time ends.
     */
     private final long biddingEnd;


     /**
     * The time when the reveal time ends.
     */
     private final long revealEnd;


     /**
     * The bidder with the highest bid, at reveal time.
     */
     private PayableContract highestBidder;


     /**
     * The highest bid, at reveal time.
     */
     private BigInteger highestBid;


     /**
     * Creates a blind auction contract.
     *
     * @param biddingTime the length of the bidding time
     * @param revealTime the length of the reveal time
     */
     public @FromContract(PayableContract.class) BlindAuction(int biddingTime, int revealTime) {
          require(biddingTime >0, "Bidding time must be positive");
          require(revealTime >0, "Reveal time must be positive");


          this.beneficiary = (PayableContract) caller();
          this.biddingEnd = now() + biddingTime;
          this.revealEnd = biddingEnd + revealTime;
     }


     /**
     * Places a blinded bid the given hash.
     * The sent money is only refunded if the bid is correctly
     * revealed in the revealing phase. The bid is valid if the
     * money sent together with the bid is at least "value" and
     * "fake" is not true. Setting "fake" to true and sending
     * not the exact amount are ways to hide the real bid but
     * still make the required deposit. The same bidder can place multiple bids.
     */
     public @Payable @FromContract(PayableContract.class) void bid(BigInteger amount, Bytes32Snapshot hash) {
          onlyBefore(biddingEnd);
          bids.computeIfAbsent((PayableContract) caller(),
              (Supplier<? extends StorageList<Bid>>) StorageLinkedList::new).add(new Bid(hash, amount));
     }


     /**
     * Reveals a bid of the caller. The caller will get a refund for all
     * correctly blinded invalid bids and for all bids except
     * for the totally highest.
     *
     * @param revealed the revealed bid
     * @throws NoSuchAlgorithmException if the hashing algorithm is not available
     */
     public @FromContract(PayableContract.class) void reveal(RevealedBid revealed)
               throws NoSuchAlgorithmException {
          onlyAfter(biddingEnd);
          onlyBefore(revealEnd);
          PayableContract bidder = (PayableContract) caller();
          StorageList<Bid> bids = this.bids.get(bidder);
          require(bids != null && bids.size() >0, "No bids to reveal");
          require(revealed != null, () -> "The revealed bid cannot be null");


          // any other hashing algorithm will do, as long as both
          // bidder and auction contracts use the same
          var digest = new SHA256Digest();
          // by removing the head of the list, it makes it impossible for the caller
          // to re-claim the same deposits
          bidder.receive(refundFor(bidder, bids.removeFirst(), revealed, digest));
     }


     public PayableContract auctionEnd() {
          onlyAfter(revealEnd);
          PayableContract winner = highestBidder;


          if (winner != null) {
               beneficiary.receive(highestBid);
               event(new AuctionEnd(winner, highestBid));
               highestBidder = null;
          }


          return winner;
     }


     /**
     * Checks how much of the deposit should be refunded for a given bid.
     *
     * @param bidder the bidder that placed the bid
     * @param bid the bid, as was placed at bidding time
     * @param revealed the bid, as was revealed later
     * @param digest the hashing algorithm
     * @return the amount to refund
     */
     private BigInteger refundFor(PayableContract bidder, Bid bid, RevealedBid revealed,
                                          SHA256Digest digest) {
          if (!bid.matches(revealed, digest))
               // the bid was not actually revealed: no refund
               return BigInteger.ZERO;
          else if (!revealed.fake && BigIntegerSupport.compareTo(bid.deposit, revealed.value) >= 0
                      && placeBid(bidder, revealed.value))
               // the bid was correctly revealed and is the best up to now:
               // only the difference between promised and provided is refunded;
               // the rest might be refunded later if a better bid will be revealed
               return BigIntegerSupport.subtract(bid.deposit, revealed.value);
          else
               // the bid was correctly revealed and is not the best one:
               // it is fully refunded
               return bid.deposit;
     }


     /**
     * Takes note that a bidder has correctly revealed a bid for the given value.
     *
     * @param bidder the bidder
     * @param value the value, as revealed
     * @return true if and only if this is the best bid, up to now
     */
     private boolean placeBid(PayableContract bidder, BigInteger value) {
          if (highestBid != null && BigIntegerSupport.compareTo(value, highestBid) <= 0)
               // this is not the best bid seen so far
               return false;


          // if there was a best bidder already, its bid is refunded
          if (highestBidder != null)
               // refund the previously highest bidder
               highestBidder.receive(highestBid);


          // take note that this is the best bid up to now
          highestBid = value;
          highestBidder = bidder;
          event(new BidIncrease(bidder, value));


          return true;
     }


     private static void onlyBefore(long when) {
          long diff = now() - when;
          require(diff <= 0, StringSupport.concat(diff, " ms too late"));
     }


     private static void onlyAfter(long when) {
          long diff = now() - when;
          require(diff >= 0, StringSupport.concat(-diff, " ms too early"));
     }
}

Let us discuss this (long) code, by starting from the inner classes.

Class Bid represents a bid placed by a contract that takes part in the auction. This information will be stored in blockchain at bidding time, hence it is known to all other participants. An instance of Bid contains the deposit paid at time of placing the bid. This is not necessarily the real value of the offer but must be at least as large as the real offer, or otherwise the bid will be considered as invalid and rejected at reveal time. Instances of Bid contain a hash consisting of \( 32 \) bytes. As already said, this will be recomputed at reveal time and matched against the result. Since arrays cannot be stored in blockchain, we use the storage class io.takamaka.code.util.Bytes32Snapshot here, a library class that holds \( 32 \) bytes, as a traditional array (see Sec. 5.2.4). It is well possible to use a StorageArray of a wrapper of byte here, but Bytes32Snapshot is much more compact and its methods consume less gas.

Class RevealedBid describes a bid revealed after bidding time. It contains the real value of the bid, the salt used to strengthen the hashing algorithm and a boolean fake that, when true, means that the bid must be considered as invalid, since it was only placed in order to confuse other bidders. It is possible to recompute and check the hash of a revealed bid through method matches(), that uses a given hashing algorithm (digest, a Java java.security.MessageDigest) to hash value, fake mark and salt into bytes, finally compared against the hash provided at bidding time.

The BlindAuction contract stores the beneficiary of the auction. It is the contract that created the auction and is consequently initialized, in the constructor of BlindAuction, to its caller. The constructor must be annotated as @FromContract because of that. The same constructor receives the length of bidding time and reveal time, in milliseconds. This allows the contract to compute the absolute ending time for the bidding phase and for the reveal phase, stored into fields biddingEnd and revealEnd, respectively. Note, in the constructor of BlindAuction, the use of the static method io.takamaka.code.lang.Takamaka.now(), that yields the current time, as with the traditional System.currentTimeMillis() of Java (that instead cannot be used in Takamaka code). Method now(), in a blockchain, yields the time of creation of the block of the current transaction, as seen by its miner. That time is reported in the block and hence is independent from the machine that runs the contract, which guarantees determinism.

Method bid() allows a caller (the bidder) to place a bid during the bidding phase. An instance of Bid is created and added to a list, specific to each bidder. Here is where our map comes to help. Namely, field bids holds a StorageTreeMap>, that can be held in the store of a node since it is a storage map between storage keys and storage values. Method bid() computes an empty list of bids if it is the first time that a bidder places a bid. For that, it uses method computeIfAbsent() of StorageMap. If it used method get(), it would run into a null-pointer exception the first time a bidder places a bid. That is, storage maps default to null, as all Java maps. (But differently to Solidity maps, that provide a default value automatically when undefined.)

Method reveal() is called by each bidder during the reveal phase. It accesses the bids placed by the bidder during the bidding time. The method matches each revealed bid against the corresponding list of bids for the player, by calling method refundFor(), that determines how much of the deposit must be refunded to the bidder. Namely, if a bid was fake or was not the best bid, it must be refunded in full. If it was the best bid, it must be partially refunded if the apparent deposit turns out to be higher than the actual value of the revealed bid. While bids are refunded, method placeBid updates the best bid information.

Method auctionEnd() is meant to be called after the reveal phase. If there is a winner, it sends the highest bid to the beneficiary.

Note the use of methods onlyBefore() and onlyAfter() to guarantee that some methods are only run at the right moment.

5.3.2 Events

(See the io-hotmoka-tutorial-examples-auction_events project in https://github.com/Hotmoka/hotmoka)

The code in the previous section does not compile since it misses two classes BidIncrease.java and AuctionEnd.java, that we report below. Namely, the code of the blind auction contract contains some lines that generate events, such as:

event(new AuctionEnd(winner, highestBid));

Events are milestones that are saved in the store of a Hotmoka node. From outside the node, it is possible to subscribe to specific events and get notified as soon as an event of that kind occurs, to trigger actions when that happens. In terms of the Takamaka language, events are generated through the io.takamaka.code.lang.Takamaka.event(Event event) method, that receives a parameter of type io.takamaka.code.lang.Event. The latter is simply an abstract class that extends Storage. Hence, events will be stored in the node as part of the transaction that generated that event. The constructor of class Event is annotated as @FromContract, which allows one to create events from the code of contracts only. The creating contract is available through method creator() of class Event.

In our example, the BlindAuction class uses two events, that you can add to the auction package and are defined as follows:

package io.hotmoka.tutorial.examples.auction;


import java.math.BigInteger;


import io.takamaka.code.lang.FromContract;
import io.takamaka.code.lang.Event;
import io.takamaka.code.lang.PayableContract;
import io.takamaka.code.lang.View;


public class BidIncrease extends Event {
    public final PayableContract bidder;
    public final BigInteger amount;


    @FromContract BidIncrease(PayableContract bidder, BigInteger amount) {
        this.bidder = bidder;
        this.amount = amount;
    }


    public @View PayableContract getBidder() {
        return bidder;
    }


    public @View BigInteger getAmount() {
        return amount;
    }
}

and

package io.hotmoka.tutorial.examples.auction;


import java.math.BigInteger;


import io.takamaka.code.lang.FromContract;
import io.takamaka.code.lang.Event;
import io.takamaka.code.lang.PayableContract;
import io.takamaka.code.lang.View;


public class AuctionEnd extends Event {
    public final PayableContract highestBidder;
    public final BigInteger highestBid;


    @FromContract AuctionEnd(PayableContract highestBidder, BigInteger highestBid) {
        this.highestBidder = highestBidder;
        this.highestBid = highestBid;
    }


    public @View PayableContract getHighestBidder() {
        return highestBidder;
    }


    public @View BigInteger getHighestBid() {
    return highestBid;
    }
}

Now that all classes have been completed, the project should compile. Go inside the project io-hotmoka-tutorial-examples-auction and run mvn install.

5.3.3 Running the blind auction contract

(See the io-hotmoka-tutorial-examples-runs project in https://github.com/Hotmoka/hotmoka)

This section presents a Java class that connects to a Hotmoka node and runs the blind auction contract of the previous section. We could run it in the ws://panarea.hotmoka.io:8001 server, but that Hotmoka node is based on a proof of space consensus, that generates a block every ten seconds on average. This means that, for a transaction to be committed, one could have to wait more, sometime up to one minute. This would make the test slow and would require larger windows for the bidding and for the revealing phases. Instead, we use the ws://panarea.hotmoka.io:8002 server, that is a Hotmoka node based on a proof of stake consensus, that generates a block every four seconds. This makes the test faster and the timings reliable. However, this means that we must first generate some new accounts for our tests, since those that we generated before for ws://panarea.hotmoka.io:8001 do not exist in ws://panarea.hotmoka.io:8002. We do it as previously done, but swapping the server we are talking to:

moka keys create --name=account4.pem --password
Enter value for --password (the password that will be needed later to use the key pair): banana
The new key pair has been written into "account4.pem":
* public key: GR2H2HhV9C6yRY8AdRX3rnLt6qyqAUY4tmRnLw6gPQzU (ed25519, base58)
* public key: 5Qavtc0QBfzjISIL5EHPgjurCTbN7jpqVAsin9z3kkE= (ed25519, base64)
* Tendermint-like address: 67C5EE5F90BBB8FF252337BCD9FB246E36C2B39B
moka keys create --name=account5.pem --password
Enter value for --password (the password that will be needed later to use the key pair): mango
The new key pair has been written into "account5.pem":
* public key: EZccGMTsEhN7TBX2hFcY697X4euwfoV8eXyUcnzLd3wd (ed25519, base58)
* public key: yYLHLawt42HaqXAr2IK2EdZciNMeRnfsuOIwv/kKQNg= (ed25519, base64)
* Tendermint-like address: DCECCEB0841487F043A0CA01B43A0FF9F20AE947
moka keys create --name=account6.pem --password
Enter value for --password (the password that will be needed later to use the key pair): strawberry
The new key pair has been written into "account6.pem":
* public key: mBD6367w4xgguzBCgF9uJ5ys4BcN2usoAzU8e4tqUAc (ed25519, base58)
* public key: C1EZKAIEkqk7lASKkYFqB6/+as72Tr2C1LYveixAEUk= (ed25519, base64)
* Tendermint-like address: 45C848069B5FEB8E023C1B46C0C61A8BCF31D841
moka accounts create faucet 50000000000000 account4.pem --password --uri ws://panarea.hotmoka.io:8002
Enter value for --password (the password of the key pair): banana
A new account 8173982155b864f1a12ebe1f9febade73bd9ebf42fd66fef6283bff2038bf728#0 has been created.
Its key pair has been saved into the file "8173982155b864f1a12ebe1f9febade73bd9ebf42fd66fef6283bff2038bf728#0.pem".


Gas consumption:
 * total: 58912
   * for CPU: 15870
   * for RAM: 7282
   * for storage: 35760
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 58912 panas
moka accounts create faucet 50000000000000 account5.pem --password --uri ws://panarea.hotmoka.io:8002
Enter value for --password (the password of the key pair): mango
A new account 97aa0e09a2d59f6c37895cce0b7bd3d4b21c77e7851d8c73ac527553fba773ea#0 has been created.
Its key pair has been saved into the file "97aa0e09a2d59f6c37895cce0b7bd3d4b21c77e7851d8c73ac527553fba773ea#0.pem".


Gas consumption:
 * total: 59952
   * for CPU: 15870
   * for RAM: 7282
   * for storage: 36800
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 59952 panas
moka accounts create faucet 50000000000000 account6.pem --password --uri ws://panarea.hotmoka.io:8002
Enter value for --password (the password of the key pair): strawberry
A new account 048998b700cb947fe3cebe37d83d5b103d12d60d80e6611ca780408f12cddac2#0 has been created.
Its key pair has been saved into the file "048998b700cb947fe3cebe37d83d5b103d12d60d80e6611ca780408f12cddac2#0.pem".


Gas consumption:
 * total: 59952
   * for CPU: 15870
   * for RAM: 7282
   * for storage: 36800
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 59952 panas

Go to the io-hotmoka-tutorial-examples-runs Eclipse project and add the following class inside its package:

package io.hotmoka.tutorial.examples.runs;


import static io.hotmoka.helpers.Coin.panarea;
import static io.hotmoka.node.StorageTypes.BIG_INTEGER;
import static io.hotmoka.node.StorageTypes.BOOLEAN;
import static io.hotmoka.node.StorageTypes.BYTE;
import static io.hotmoka.node.StorageTypes.BYTES32_SNAPSHOT;
import static io.hotmoka.node.StorageTypes.INT;
import static io.hotmoka.node.StorageTypes.PAYABLE_CONTRACT;
import static io.hotmoka.node.StorageValues.byteOf;


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 java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;


import io.hotmoka.constants.Constants;
import io.hotmoka.crypto.api.Signer;
import io.hotmoka.helpers.GasHelpers;
import io.hotmoka.helpers.NonceHelpers;
import io.hotmoka.helpers.SignatureHelpers;
import io.hotmoka.helpers.api.GasHelper;
import io.hotmoka.helpers.api.NonceHelper;
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.signatures.ConstructorSignature;
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 Auction {


    public final static int NUM_BIDS = 10; // number of bids placed
    public final static int BIDDING_TIME = 230_000; // in milliseconds
    public final static int REVEAL_TIME = 350_000; // in milliseconds


    private final static BigInteger _500_000 = BigInteger.valueOf(500_000);


    private final static ClassType BLIND_AUCTION
         = StorageTypes.classNamed("io.hotmoka.tutorial.examples.auction.BlindAuction");
    private final static ConstructorSignature CONSTRUCTOR_BYTES32_SNAPSHOT
         = ConstructorSignatures.of(BYTES32_SNAPSHOT,
              BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE,
              BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE,
              BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE,
              BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE, BYTE);


    private final TransactionReference takamakaCode;
    private final StorageReference[] accounts;
    private final List<Signer<SignedTransactionRequest<?>>> signers = new ArrayList<>();
    private final String chainId;
    private final long start; // the time when bids started being placed
    private final Node node;
    private final TransactionReference classpath;
    private final StorageReference auction;
    private final List<BidToReveal> bids = new ArrayList<>();
    private final GasHelper gasHelper;
    private final NonceHelper nonceHelper;


    public static void main(String[] args) throws Exception {
    try (Node node = RemoteNodes.of(new URI(args[0]), 20000)) {
              new Auction(node, Paths.get(args[1]),
                StorageValues.reference(args[2]), args[3],
                StorageValues.reference(args[4]), args[5],
                StorageValues.reference(args[6]), args[7]);
         }
    }


    /**
    * Class used to keep in memory the bids placed by each player,
    * that will be revealed at the end.
    */
    private class BidToReveal {
         private final int player;
         private final BigInteger value;
         private final boolean fake;
         private final byte[] salt;


         private BidToReveal(int player, BigInteger value, boolean fake, byte[] salt) {
              this.player = player;
              this.value = value;
              this.fake = fake;
              this.salt = salt;
         }


         /**
         * Creates in store a revealed bid corresponding to this object.
         *
         * @return the storage reference to the freshly created revealed bid
         */
         private StorageReference intoBlockchain() throws Exception {
              StorageReference bytes32 = node.addConstructorCallTransaction(TransactionRequests.constructorCall
                (signers.get(player), accounts[player],
                nonceHelper.getNonceOf(accounts[player]), chainId, _500_000,
                panarea(gasHelper.getSafeGasPrice()), classpath, CONSTRUCTOR_BYTES32_SNAPSHOT,
                byteOf(salt[0]), byteOf(salt[1]), byteOf(salt[2]), byteOf(salt[3]),
                byteOf(salt[4]), byteOf(salt[5]), byteOf(salt[6]), byteOf(salt[7]),
                byteOf(salt[8]), byteOf(salt[9]), byteOf(salt[10]), byteOf(salt[11]),
                byteOf(salt[12]), byteOf(salt[13]), byteOf(salt[14]), byteOf(salt[15]),
                byteOf(salt[16]), byteOf(salt[17]), byteOf(salt[18]), byteOf(salt[19]),
                byteOf(salt[20]), byteOf(salt[21]), byteOf(salt[22]), byteOf(salt[23]),
                byteOf(salt[24]), byteOf(salt[25]), byteOf(salt[26]), byteOf(salt[27]),
                byteOf(salt[28]), byteOf(salt[29]), byteOf(salt[30]), byteOf(salt[31])));


              var CONSTRUCTOR_REVEALED_BID
                = ConstructorSignatures.of(
                      StorageTypes.classNamed("io.hotmoka.tutorial.examples.auction.BlindAuction$RevealedBid"),
                      BIG_INTEGER, BOOLEAN, BYTES32_SNAPSHOT);


              return node.addConstructorCallTransaction(TransactionRequests.constructorCall
                (signers.get(player), accounts[player],
                nonceHelper.getNonceOf(accounts[player]), chainId,
                _500_000, panarea(gasHelper.getSafeGasPrice()), classpath, CONSTRUCTOR_REVEALED_BID,
                StorageValues.bigIntegerOf(value), StorageValues.booleanOf(fake), bytes32));
         }
    }


    private Auction(Node node, Path dir, StorageReference account1, String password1,
              StorageReference account2, String password2, StorageReference account3, String password3)
              throws Exception {


         this.node = node;
         takamakaCode = node.getTakamakaCode();
         accounts = new StorageReference[] { account1, account2, account3 };
         var signature = node.getConfig().getSignatureForRequests();
         Function<? super SignedTransactionRequest<?>, byte[]> toBytes
              = SignedTransactionRequest<?>::toByteArrayWithoutSignature;
         signers.add(signature.getSigner(loadKeys(node, dir, account1, password1).getPrivate(), toBytes));
         signers.add(signature.getSigner(loadKeys(node, dir, account2, password2).getPrivate(), toBytes));
         signers.add(signature.getSigner(loadKeys(node, dir, account3, password3).getPrivate(), toBytes));
         gasHelper = GasHelpers.of(node);
         nonceHelper = NonceHelpers.of(node);
         chainId = node.getConfig().getChainId();
         classpath = installJar();
         auction = createContract();
         start = System.currentTimeMillis();


         StorageReference expectedWinner = placeBids();
         waitUntilEndOfBiddingTime();
         revealBids();
         waitUntilEndOfRevealTime();
         StorageValue winner = askForWinner();


         // show that the contract computes the correct winner
         System.out.println("expected winner: " + expectedWinner);
         System.out.println("actual winner: " + winner);
    }


    private StorageReference createContract() throws Exception {
         System.out.println("Creating contract");


         var CONSTRUCTOR_BLIND_AUCTION = ConstructorSignatures.of(BLIND_AUCTION, INT, INT);


         return node.addConstructorCallTransaction
              (TransactionRequests.constructorCall(signers.get(0), accounts[0],
              nonceHelper.getNonceOf(accounts[0]), chainId, _500_000, panarea(gasHelper.getSafeGasPrice()),
              classpath, CONSTRUCTOR_BLIND_AUCTION,
              StorageValues.intOf(BIDDING_TIME), StorageValues.intOf(REVEAL_TIME)));
    }


    private TransactionReference installJar() throws Exception {
         System.out.println("Installing jar");


         //the path of the user jar to install
         var auctionPath = Paths.get(System.getProperty("user.home")
              + "/.m2/repository/io/hotmoka/io-hotmoka-tutorial-examples-auction/"
              + Constants.HOTMOKA_VERSION
              + "/io-hotmoka-tutorial-examples-auction-" + Constants.HOTMOKA_VERSION + ".jar");


         return node.addJarStoreTransaction(TransactionRequests.jarStore
              (signers.get(0), // an object that signs with the payer's private key
              accounts[0], // payer
              nonceHelper.getNonceOf(accounts[0]), // payer's nonce
              chainId, // chain identifier
              BigInteger.valueOf(5_000_000), // gas limit: enough for this 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(auctionPath), // bytes of the jar to install
              takamakaCode)); // dependency
    }


    private StorageReference placeBids() throws Exception {
         var maxBid = BigInteger.ZERO;
         StorageReference expectedWinner = null;
         var random = new Random();
         var BID = MethodSignatures.ofVoid(BLIND_AUCTION, "bid", BIG_INTEGER, BYTES32_SNAPSHOT);


         int i = 1;
         while (i <= NUM_BIDS) { // generate NUM_BIDS random bids
              System.out.println("Placing bid " + i + "/" + NUM_BIDS);
              int player = 1 + random.nextInt(accounts.length - 1);
              var deposit = BigInteger.valueOf(random.nextInt(1000));
              var value = BigInteger.valueOf(random.nextInt(1000));
              boolean fake = random.nextInt(100) >= 80;
              var salt = new byte[32];
              random.nextBytes(salt); // random 32 bytes of salt for each bid


              // create a Bytes32 hash of the bid in the store of the node
              StorageReference bytes32 = codeAsBytes32(player, value, fake, salt);


              // keep note of the best bid, to verify the result at the end
              if (!fake && deposit.compareTo(value) >= 0)
                if (expectedWinner == null || value.compareTo(maxBid) >0) {
                     maxBid = value;
                     expectedWinner = accounts[player];
                }
                else if (value.equals(maxBid))
                     // we do not allow ex aequos, since the winner
                     // would depend on the fastest player to reveal
                     continue;


              // keep the explicit bid in memory, not yet in the node,
              // since it would be visible there
              bids.add(new BidToReveal(player, value, fake, salt));


              // place a hashed bid in the node
              node.addInstanceMethodCallTransaction(TransactionRequests.instanceMethodCall
                (signers.get(player), accounts[player],
                nonceHelper.getNonceOf(accounts[player]), chainId,
                _500_000, panarea(gasHelper.getSafeGasPrice()), classpath, BID,
                auction, StorageValues.bigIntegerOf(deposit), bytes32));


              i++;
         }


         return expectedWinner;
    }


    private void revealBids() throws Exception {
         var REVEAL = MethodSignatures.ofVoid
              (BLIND_AUCTION, "reveal",
               StorageTypes.classNamed("io.hotmoka.tutorial.examples.auction.BlindAuction$RevealedBid"));


         // we create the revealed bids in blockchain; this is safe now, since the bidding time is over
         int counter = 1;
         for (BidToReveal bid: bids) {
              System.out.println("Revealing bid " + counter++ + "/" + bids.size());
              int player = bid.player;
              StorageReference bidInBlockchain = bid.intoBlockchain();
              node.addInstanceMethodCallTransaction(TransactionRequests.instanceMethodCall
                (signers.get(player), accounts[player],
                nonceHelper.getNonceOf(accounts[player]), chainId, _500_000,
                panarea(gasHelper.getSafeGasPrice()),
                classpath, REVEAL, auction, bidInBlockchain));
         }
    }


    private StorageReference askForWinner() throws Exception {
         var AUCTION_END = MethodSignatures.ofNonVoid
              (BLIND_AUCTION, "auctionEnd", PAYABLE_CONTRACT);


         StorageValue winner = node.addInstanceMethodCallTransaction
              (TransactionRequests.instanceMethodCall
              (signers.get(0), accounts[0], nonceHelper.getNonceOf(accounts[0]),
              chainId, _500_000, panarea(gasHelper.getSafeGasPrice()),
              classpath, AUCTION_END, auction)).get();


         // the winner is normally a StorageReference,
         // but it could be a NullValue if all bids were fake
         return winner instanceof StorageReference sr ? sr : null;
    }


    private void waitUntilEndOfBiddingTime() {
         waitUntil(BIDDING_TIME + 10000, "Waiting until the end of the bidding time");
    }


    private void waitUntilEndOfRevealTime() {
         waitUntil(BIDDING_TIME + REVEAL_TIME + 10000, "Waiting until the end of the revealing time");
    }


    /**
    * Waits until a specific time after start.
    */
    private void waitUntil(long duration, String forWhat) {
         long msToWait = start + duration - System.currentTimeMillis();
         System.out.println(forWhat + " (" + msToWait + "ms still missing)");
    try {
              Thread.sleep(msToWait);
         }
         catch (InterruptedException e) {
              Thread.currentThread().interrupt();
         }
    }


    /**
    * Hashes a bid and put it in the store of the node, in hashed form.
    */
    private StorageReference codeAsBytes32(int player, BigInteger value, boolean fake, byte[] salt)
              throws Exception {
    // the hashing algorithm used to hide the bids
    var digest = MessageDigest.getInstance("SHA-256");
         digest.update(value.toByteArray());
         digest.update(fake ? (byte) 0 : (byte) 1);
         digest.update(salt);
         byte[] hash = digest.digest();
         return createBytes32(player, hash);
    }


    /**
    * Creates a Bytes32Snapshot object in the store of the node.
    */
    private StorageReference createBytes32(int player, byte[] hash) throws Exception {
         return node.addConstructorCallTransaction
              (TransactionRequests.constructorCall(
              signers.get(player),
              accounts[player],
              nonceHelper.getNonceOf(accounts[player]), chainId,
              _500_000, panarea(gasHelper.getSafeGasPrice()),
              classpath, CONSTRUCTOR_BYTES32_SNAPSHOT,
              byteOf(hash[0]), byteOf(hash[1]),
              byteOf(hash[2]), byteOf(hash[3]),
              byteOf(hash[4]), byteOf(hash[5]),
              byteOf(hash[6]), byteOf(hash[7]),
              byteOf(hash[8]), byteOf(hash[9]),
              byteOf(hash[10]), byteOf(hash[11]),
              byteOf(hash[12]), byteOf(hash[13]),
              byteOf(hash[14]), byteOf(hash[15]),
              byteOf(hash[16]), byteOf(hash[17]),
              byteOf(hash[18]), byteOf(hash[19]),
              byteOf(hash[20]), byteOf(hash[21]),
              byteOf(hash[22]), byteOf(hash[23]),
              byteOf(hash[24]), byteOf(hash[25]),
              byteOf(hash[26]), byteOf(hash[27]),
              byteOf(hash[28]), byteOf(hash[29]),
              byteOf(hash[30]), byteOf(hash[31])));
    }


    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));
    }
}

This test class is relatively long and complex. Let us start from its beginning. The code specifies that the test will place 10 random bids, that the bidding phase lasts \( 100 \) seconds and that the reveal phase lasts \( 140 \) seconds (these timings are fine on a blockchain that creates a block every four seconds; shorter block creation times would allow shorter timings):

public final static int NUM_BIDS = 10;
public final static int BIDDING_TIME = 230_000;
public final static int REVEAL_TIME = 350_000;

Some constant signatures follow, that simplify the calls to methods and constructors later. Method main() connects to a remote node and passes it as a parameter to the constructor of class Auction, that installs io-hotmoka-tutorial-examples-auction-1.11.5.jar inside it. It stores the node in field node. Then the constructor of Auction creates an auction contract in the node and calls method placeBids() that uses the inner class BidToReveal to keep track of the bids placed during the test, in clear. Initially, bids are kept in memory, not in the store of the node, where they could be publicly accessed. Only their hashes are stored in the node. Method placeBids() generates NUM_BIDS random bids on behalf of the accounts.length - 1 players (the first element of the accounts array is the creator of the auction):

int i = 1;
while (i <= NUM_BIDS) {
    int player = 1 + random.nextInt(accounts.length - 1);
    var deposit = BigInteger.valueOf(random.nextInt(1000));
    var value = BigInteger.valueOf(random.nextInt(1000));
    var fake = random.nextInt(100) >= 80; // fake in 20% of the cases
    var salt = new byte[32];
    random.nextBytes(salt);
    ...
}

Each random bid is hashed (including a random salt) and a Bytes32Snapshot object is created in the store of the node, containing that hash:

StorageReference bytes32 = codeAsBytes32(player, value, fake, salt);

The bid, in clear, is added to a list bids that, at the end of the loop, will contain all bids:

bids.add(new BidToReveal(player, value, fake, salt));

The hash is used instead to place a bid in the node:

node.addInstanceMethodCallTransaction(TransactionRequests.instanceMethodCall
  (signers.get(player), accounts[player],
  nonceHelper.getNonceOf(accounts[player]), chainId,
  _500_000, panarea(gasHelper.getSafeGasPrice()), classpath, BID,
  auction, StorageValues.bigIntegerOf(deposit), bytes32));

The loop takes also care of keeping track of the best bidder, that placed the best bid, so that it can be compared at the end with the best bidder computed by the smart contract (they should coincide):

if (!fake && deposit.compareTo(value) >= 0)
  if (expectedWinner == null || value.compareTo(maxBid) >0) {
      maxBid = value;
      expectedWinner = accounts[player];
  }
  else if (value.equals(maxBid))
      continue;

As you can see, the test above avoids generating a bid that is equal to the best bid seen so far. This avoids having two bidders that place the same bid: the smart contract will consider as winner the first bidder that reveals its bids. To avoid this tricky case, we prefer to assume that the best bid is unique. This is just a simplification of the testing code, since the smart contract deals perfectly with that case.

After all bids have been placed, the constructor of Auction waits until the end of the bidding time:

waitUntilEndOfBiddingTime();

Then it calls method revealBids(), that reveals the bids to the smart contract, in plain. It creates in the store of the node a data structure RevealedBid for each elements of the list bids, by calling bid.intoBlockchain(). This creates the bid in clear in the store of the node, but this is safe now, since the bidding time is over and they cannot be used to guess a winning bid anymore. Then method revealBids() reveals the bids by calling method reveal() of the smart contract:

for (BidToReveal bid: bids) {
    int player = bid.player;
    StorageReference bidInBlockchain = bid.intoBlockchain();
    node.addInstanceMethodCallTransaction(TransactionRequests.instanceMethodCall
      (signers.get(player), accounts[player],
      nonceHelper.getNonceOf(accounts[player]), chainId, _500_000,
      panarea(gasHelper.getSafeGasPrice()),
      classpath, REVEAL, auction, bidInBlockchain));
}

Note that this is possible since the inner class RevealedBid of the smart contract has been annotated as @Exported (see its code in Sec. 5.3.1), hence its instances can be passed as argument to calls from outside the blockchain.

Subsequently, the constructor of Auction waits until the end of the reveal phase:

waitUntilEndOfRevealTime();

After that, method askForWinner() signals to the smart contract that the auction is over and asks about the winner:

StorageValue winner = node.addInstanceMethodCallTransaction
  (TransactionRequests.instanceMethodCall
  (signers.get(0), accounts[0], nonceHelper.getNonceOf(accounts[0]),
  chainId, _500_000, panarea(gasHelper.getSafeGasPrice()),
  classpath, AUCTION_END, auction)).get();

The final two System.out.println()’ss in the constructor of Auction allow one to verify that the smart contract actually computes the right winner, since they will always print the identical storage object (different at each run, in general), such as:

expected winner: 97aa0e09a2d59f6c…#0
actual winner: 97aa0e09a2d59f6c…#0

We can run class Auction now (please note that the execution of this test will take a few minutes):

mvn compile exec:java -Dexec.mainClass="io.hotmoka.tutorial.examples.runs.Auction" -Dexec.args="ws://panarea.hotmoka.io:8002 hotmoka_tutorial 8173982155b864f1a12ebe1f9febade73bd9ebf42fd66fef6283bff2038bf728#0 banana 97
aa0e09a2d59f6c37895cce0b7bd3d4b21c77e7851d8c73ac527553fba773ea#0 mango 048998b700cb947fe3cebe37d83d5b103d12d60d80e6611ca780408f12cddac2#0 strawberry"

Its execution should print something like this on the console:

Installing jar
Creating contract
Placing bid 1/10
Placing bid 2/10
Placing bid 3/10
Placing bid 4/10
Placing bid 5/10
Placing bid 6/10
Placing bid 7/10
Placing bid 8/10
Placing bid 9/10
Placing bid 10/10
Waiting until the end of the bidding time (38597ms still missing)
Revealing bid 1/10
Revealing bid 2/10
Revealing bid 3/10
Revealing bid 4/10
Revealing bid 5/10
Revealing bid 6/10
Revealing bid 7/10
Revealing bid 8/10
Revealing bid 9/10
Revealing bid 10/10
Waiting until the end of the revealing time (55714ms still missing)
expected winner: 97aa0e09a2d59f6c37895cce0b7bd3d4b21c77e7851d8c73ac527553fba773ea#0
actual winner: 97aa0e09a2d59f6c37895cce0b7bd3d4b21c77e7851d8c73ac527553fba773ea#0
5.3.4 Listening to events

(See the io-hotmoka-tutorial-examples-runs project in https://github.com/Hotmoka/hotmoka)

The BlindAuction contract generates events during its execution. If an external tool, such as a wallet, wants to listen to such events and trigger some activity when they occur, it is enough for it to subscribe to the events of a node that is executing the contract, by providing a handler that gets executed each time a new event gets generated. Subscription requires to specify the creator of the events that should be forwarded to the handler. In our case, this is the auction contract. Thus, clone the Auction.java class into Events.java and modify its constructor as follows:

...
import io.hotmoka.node.api.ClosedNodeException;
import io.hotmoka.node.api.UnknownReferenceException;
...
auction = createAuction();
start = System.currentTimeMillis();


try (var subscription = node.subscribeToEvents(auction, this::eventHandler)) {
    StorageReference expectedWinner = placeBids();
    waitUntilEndOfBiddingTime();
    revealBids();
    waitUntilEndOfRevealTime();
    StorageValue winner = askForWinner();


    System.out.println("expected winner: " + expectedWinner);
    System.out.println("actual winner: " + winner);


    waitUntilAllEventsAreFlushed();
}


private void waitUntilAllEventsAreFlushed() {
    waitUntil(BIDDING_TIME + REVEAL_TIME + 30000, "Waiting until all events are flushed");
}


private void eventHandler(StorageReference creator, StorageReference event) {
    try {
        System.out.println
          ("Seen event of class " + node.getClassTag(event).getClazz()
            + " created by contract " + creator);
    }
    catch (ClosedNodeException | UnknownReferenceException | TimeoutException e) {
        System.out.println("The node is misbehaving: " + e.getMessage());
    }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

The event handler, in this case, simply prints on the console the class of the event and its creator (that will coincide with auction). You can run the Events class now:

mvn compile exec:java -Dexec.mainClass="io.hotmoka.tutorial.examples.runs.Events" -Dexec.args="ws://panarea.hotmoka.io:8002 hotmoka_tutorial 8173982155b864f1a12ebe1f9febade73bd9ebf42fd66fef6283bff2038bf728#0 banana 97
aa0e09a2d59f6c37895cce0b7bd3d4b21c77e7851d8c73ac527553fba773ea#0 mango 048998b700cb947fe3cebe37d83d5b103d12d60d80e6611ca780408f12cddac2#0 strawberry"

You should see something like this on the console:

Installing jar
Creating contract
Placing bid 1/10
Placing bid 2/10
Placing bid 3/10
Placing bid 4/10
Placing bid 5/10
Placing bid 6/10
Placing bid 7/10
Placing bid 8/10
Placing bid 9/10
Placing bid 10/10
Waiting until the end of the bidding time (33460ms still missing)
Revealing bid 1/10
Revealing bid 2/10
Revealing bid 3/10
Revealing bid 4/10
Revealing bid 5/10
Seen event of class io.hotmoka.tutorial.examples.auction.BidIncrease created by contract 4b4e839fb418d6b706d0a61fb2c32f2a74b1c9541c13e06e8dbc58d4d2ee9969#0
Revealing bid 6/10
Revealing bid 7/10
Seen event of class io.hotmoka.tutorial.examples.auction.BidIncrease created by contract 4b4e839fb418d6b706d0a61fb2c32f2a74b1c9541c13e06e8dbc58d4d2ee9969#0
Revealing bid 8/10
Seen event of class io.hotmoka.tutorial.examples.auction.BidIncrease created by contract 4b4e839fb418d6b706d0a61fb2c32f2a74b1c9541c13e06e8dbc58d4d2ee9969#0
Revealing bid 9/10
Revealing bid 10/10
Waiting until the end of the revealing time (50637ms still missing)
expected winner: 048998b700cb947fe3cebe37d83d5b103d12d60d80e6611ca780408f12cddac2#0
actual winner: 048998b700cb947fe3cebe37d83d5b103d12d60d80e6611ca780408f12cddac2#0
Waiting until all events are flushed (15011ms still missing)
Seen event of class io.hotmoka.tutorial.examples.auction.AuctionEnd created by contract 4b4e839fb418d6b706d0a61fb2c32f2a74b1c9541c13e06e8dbc58d4d2ee9969#0

The subscribeToEvents() method returns a Subscription object that should be closed when it is not needed anymore, in order to reduce the overhead on the node. Since it is an AutoCloseable resource, the recommended technique is to use a try-with-resource construct, as shown in the previous example. Moreover, our code waits for a few seconds before closing the subscription to the events, in order to give events the time to be forwarded to the client.

In general, event handlers can perform arbitrarily complex operations and even access the event object in the store of the node, from its storage reference, reading its fields or calling its methods. Please remember, however, that event handlers are run in a thread of the node. Hence, they should be fast and shouldn’t hang. It is good practice to let event handlers add events in a queue, in a non-blocking way. A consumer thread, external to the node, then retrieves the events from the queue and processes them in turn.

It is possible to subscribe to all events generated by a node, by using null as creator in the subscribeToEvents() method. Think twice before doing that, since your handler will be notified of all events generated by any application installed in the node. It might be a lot.