Complementing Tokens SDK With Off-Ledger Custom Reporting
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 toHolderB
? - 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:
- I used
long
forquantity
, but it all depends on your requirement; you might use something else likedouble
orBigDecimal
. - You can pass any value for
txType
, for instance:ISSUE
,MOVE
, orREDEEM
.
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
andto_holder
; it could beParty.name
,AccountInfo.name
orAccountInfo.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.
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
— — — — — — — — —
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.