Complementing Tokens SDK With Off-Ledger Custom Reporting

Adel Rustum
B9lab blog
Published in
5 min readApr 9, 2020

--

Image by Chrystal Elizabeth from Pixabay

Preface

This is the first part of a series on adding custom reporting to Tokens SDK. Here, we demonstrate an off-ledger approach, the on-ledger approach can be explored in the second part.

Below, is a quick comparison between the 2 methods to help you decide on which approach to use before you deep dive into reading:

Introduction

R3 introduced the Tokens SDK in Corda 4.3; it’s a great addition to an already great product, and If you’re reading this article; then I would assume that you’re already familiar with it (if not, then please read this first).

The SDK exposes both FungibleToken and NonFugibleToken in custom tables (fungible_token and non_fungible_token respectively), giving you more freedom to query those states from inside of your flows, services, or even your favorite SQL tool (e.g. pgAdmin for PostgreSQL).

But unfortunately, you’re limited to queries based on the attributes of those states, and if you’re using a SQL tool; then most of the time you’d have to create joins with other node tables. For example, to exclude consumed tokens from your result set, you’d have to join your query with vault_states to check the state_status value.

But, what if we wanted a different type of queries? For example:

  • How many tokens were issued between 2 dates?
  • What’s the average quantity that HolderA moves to HolderB?
  • How frequently does HolderC redeem their tokens?

In order to achieve that, we’re going to create a table with the following columns:

As you can see, the above table has everything that we need to craft some elaborate queries; so let’s start building this stuff!

Implementation

  • Create class TokenTransactionSchema:
/**
* The family of schemas for TokenTransaction.
*/
public class TokenTransactionSchema {}
  • Create class TokenTransactionSchemaV1:
public class TokenTransactionSchemaV1 extends MappedSchema {    public TokenTransactionSchemaV1() {
super(TokenTransactionSchema.class, 1,
Collections.singletonList(
PersistentTokenTransaction.class));
}
/*
* Notice that it "implements Serializable" unlike state
* custom schemas which "extends PersistentState".
* */
@Entity
@Table(name = "token_transactions")
public static class PersistentTokenTransaction
implements Serializable {
// @Id column is required by Hibernate.
@Id
@Column(name = "id", nullable = false)
private final String id;
@Column(name = "tx_hash", nullable = false)
private final String txHash;
@Column(name = "tx_timestamp", nullable = false)
private final Instant txTimestamp;
@Column(name = "tx_type", nullable = false)
private final String txType;
@Column(name = "from_holder")
private final String fromHolder;
@Column(name = "to_holder")
private final String toHolder;
/*
* Tokens SDK uses "Amount.quantity" of type "long",
* so we'll use the same.
* */
@Column(name = "quantity", nullable = false)
private final long quantity;
public PersistentTokenTransaction(String id, String txHash,
Instant txTimestamp, String txType,
String fromHolder, String toHolder,
long quantity) {
this.id = id;
this.txHash = txHash;
this.txTimestamp = txTimestamp;
this.txType = txType;
this.fromHolder = fromHolder;
this.toHolder = toHolder;
this.quantity = quantity;
}
// Default constructor is required by hibernate.
public PersistentTokenTransaction() {
this.id = null;
this.txHash = null;
this.txTimestamp = null;
this.txType = null;
this.fromHolder = null;
this.toHolder = null;
this.quantity = 0;
}
public String getId() {
return id;
}
public String getTxHash() {
return txHash;
}
public Instant getTxTimestamp() {
return txTimestamp;
}
public String getTxType() {
return txType;
}
public String getFromHolder() {
return fromHolder;
}
public String getToHolder() {
return toHolder;
}
public long getQuantity() {
return quantity;
}
}
}

Please note in the above code:

  1. I used long for quantity, but it all depends on your requirement; you might use something else like double or BigDecimal.
  2. You can pass any value for txType, for instance: ISSUE, MOVE, or REDEEM.

You should end up with code looking like this:

  • Now we can persist the entity inside of our flows, and It’s up to you to decide on what you want to pass as values for from_holder and to_holder; it could be Party.name, AccountInfo.name or AccountInfo.identifier:
SignedTransaction moveTokensTx = subFlow(new MoveFungibleTokens(
Collections.singletonList(partyAndAmount),
Collections.emptyList(),
heldByMint, mint));
// Persist token-transaction.
PersistentTokenTransaction tokenTransaction =
new PersistentTokenTransaction(
new UniqueIdentifier().getId().toString(),
moveTokensTx.getId().toString(), Instant.now(),
"MOVE",
"Mint",
"HolderA",
100);
getServiceHub().withEntityManager(entityManager -> {
entityManager.persist(tokenTransaction);
return null;
});

Results

Using the approach demonstrated in this article, you will be able to get better reporting results on token related transactions by querying the token_transactions table.

Click on image to enlarge.

Obviously, my work is just a template that you can start with and modify it to your needs.

Disclaimer on Off-Ledger Data!

  • The data stored in the custom table is considered off-ledger.
    Even though we insert that data from inside of our flows, it’s not tracked and protected from tampering by Corda’s DLT; because it’s not like a state that resulted from a transaction which was signed by a quorum of parties and finalized by a notary.
    Unlike on-ledger data (i.e. Corda’s states); anyone can login to the node’s database and tamper with the custom table’s data without leaving an audit trail or causing an error.
    So this approach is good for a quick and simple implementation, but it doesn’t guarantee cryptographically secured immutable auditable data.
  • You must insure the atomic registration of on-ledger (token) data and off-ledger (reporting) data.
    Meaning, you must insure that when the Tokens SDK flow completes successfully; a record is also inserted successfully into your custom table. This sequence of events might be disrupted in between by a node failure or a database exception, leaving you with a discrepancy (i.e. token data exists, but reporting data doesn’t).

References

JPA Support

— — — — — — — — —

B9lab Academy has partnered with R3’s Corda to create free online learning materials. It can take you from zero blockchain knowledge to creating cordapps that work with cash, debt and other contracts, and facilitates flexible, potentially complex settlement agreements across multiple currencies and instruments.

You can get additional support from our expert instructors for 3 months to help you succeed through all stages of the training.

--

--