I am re-writing and running a IssueFlow for an example cordapp here.
I can see the flow is successful and can find a number of UTXOs on the vault query for the node.
run vaultQuery contractStateType: com.example.state.IOUStat
I would like to view the data in the persistent store (H2).
I've added the following to my node's config (similar for party A node with different port).
devMode=true
myLegalName="O=PartyB,L=New York,C=US"
p2pAddress="localhost:10008"
rpcSettings {
address="localhost:10009"
adminAddress="localhost:10049"
}
security {
authService {
dataSource {
type=INMEMORY
users=[
{
password=test
permissions=[
ALL
]
user=user1
}
]
}
}
}
h2Settings {
address: "localhost:12344"
}
I can see the DB url on run-nodes
jdbc:h2:tcp://localhost:12344/node
I can successfully connect to this db url. However , I do not see any tables for my Persistent state
DB Query
object IOUSchema
object IOUSchemaV1 : MappedSchema(
schemaFamily = IOUSchema.javaClass,
version = 1,
mappedTypes = listOf(PersistentIOU::class.java)){
#Entity
#Table(name = "iou_states")
class PersistentIOU(
#Column(name = "lender")
var lenderName : String,
#Column (name = "borrower")
var borrowerName : String,
#Column(name = "value")
var value : Int,
#Column(name = "linear_id")
var linearId : UUID
) : PersistentState(){
constructor() : this("","",0, UUID.randomUUID())
}
}
#BelongsToContract(IOUContract::class)
data class IOUState (val value : Int,
val lender: Party,
val borrower : Party,
override val linearId: UniqueIdentifier = UniqueIdentifier()): LinearState, QueryableState {
override val participants : List<AbstractParty> get() = listOf(lender,borrower)
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema){
is IOUSchemaV1 -> IOUSchemaV1.PersistentIOU(
this.lender.name.toString(),
this.borrower.name.toString(),
this.value,
this.linearId.id
)
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(IOUSchemaV1)
}
EDIT: This certainly works by connecting directly to the file. See here, However this looks like it is connecting to a different db when using a external client (dbeaver) to connect to the JDBC url emmited on node startup.
I was unable to find tables in H2 instance as my connection string was wrong & H2 appears is creating a blank db when trying to connect to a non-existant DB.
In DBeaver , you can add the JDBC url, but it automatically pre-fixes 'jdbc:h2:'
Connection details
Related
I'm currently training to become a corda developer.
I've created a simple corDapp which only has 1 participant, Here is the state I created:
#BelongsToContract(UserContract::class)
class UserState(val name: String,
val age: Int,
val address: String,
val gender: GenderEnums,
val node: Party,
val status: StatusEnums,
override val linearId: UniqueIdentifier,
override val participants : List<Party>
) : LinearState
So when I run the corDapp, I get my desired output https://imgur.com/mOKhNpI
But what I want to do is to update the vault. I would, for example, like to update the address from "Pampanga" to "Manila", But I don't know where to start. All I know is that since States are immutable, you have to consume the state first.
I tried to create a flow:
#InitiatingFlow
#StartableByRPC
class UpdateUserFlows(private val name :String,
private val age : Int,
private val address : String,
private val gender: GenderEnums,
private val status : StatusEnums,
private val counterParty: Party): FlowLogic<SignedTransaction>() {
private fun userStates(): UserState {
return UserState(
name = name,
age = age,
address = address,
gender = gender,
status = status,
node = ourIdentity,
linearId = UniqueIdentifier(),
participants = listOf(ourIdentity, counterParty)
)
}
#Suspendable
override fun call(): SignedTransaction {
val transaction: TransactionBuilder = transaction()
val signedTransaction: SignedTransaction = verifyAndSign(transaction)
val sessions: List<FlowSession> = (userStates().participants - ourIdentity).map { initiateFlow(it) }.toSet().toList()
val transactionSignedByAllParties: SignedTransaction = collectSignature(signedTransaction, sessions)
return recordTransaction(transactionSignedByAllParties, sessions)
}
private fun transaction(): TransactionBuilder {
val notary: Party = serviceHub.networkMapCache.notaryIdentities.first()
val issueCommand = Command(UserContract.Commands.Issue(), userStates().participants.map { it.owningKey })
val builder = TransactionBuilder(notary = notary)
builder.addOutputState(userStates(), UserContract.ID)
builder.addCommand(issueCommand)
return builder
}
private fun verifyAndSign(transaction: TransactionBuilder): SignedTransaction {
transaction.verify(serviceHub)
return serviceHub.signInitialTransaction(transaction)
}
#Suspendable
private fun collectSignature(
transaction: SignedTransaction,
sessions: List<FlowSession>
): SignedTransaction = subFlow(CollectSignaturesFlow(transaction, sessions))
#Suspendable
private fun recordTransaction(transaction: SignedTransaction, sessions: List<FlowSession>): SignedTransaction =
subFlow(FinalityFlow(transaction, sessions))
}
But it's not working.
You are right, states are immutable in Corda and to mimic an update you basically need to create a transaction where the input is your current version of the state (i.e. address = Pampanga), and the output would be a new instance of UserState which has the same linearId and other attribute values as the input, but a different address value (i.e. Manila).
This way you are creating a new state (the output of the transaction), but because it will have the same linearId value; it would be as if you updated the input to become the output, this will allow you to see all the previous versions of a state by querying the vault by its linearId.
You didn't share the code of your contract, but you need to add a new command (let's call it Update); and also you need to add the verification rules that apply to it.
Inside your flow, you'd use that command (instead of Issue).
it works fine if i change lateinit var id: String in the Payment.kt and CartPayment.kt to var id: String? = "", but the problem is i want the id to be required, how can i achieve that ?
The Error:
java.lang.RuntimeException: Unable to create application: io.realm.exceptions.RealmMigrationNeededException: Migration is required due to the following errors:
- Property 'CartPayment.id' has been made required.
- Property 'Payment.id' has been made required.
Model :
open class Payment() : RealmObject() {
#PrimaryKey
lateinit var id: String
var typeValue: Int = 0
var statusValue: Int = 0
var value: Double = 0.0
var referenceNumber: String? = null
Note: Payment and CartPayment models are identical except for the class name
Migration.kt
class Migration : RealmMigration {
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
var oldVersion = oldVersion
val schema = realm.schema
if (oldVersion == 0L) {
schema.create("Payment")
.addField("id", String::class.java, FieldAttribute.PRIMARY_KEY)
.addField("typeValue", Int::class.java)
.addField("statusValue", Int::class.java)
.addField("value", Double::class.java)
.addField("referenceNumber", String::class.java)
schema.get("Order")!!
.addRealmListField("payments", schema.get("Payment")!!)
oldVersion++
}
if (oldVersion == 1L) {
schema.create("CartPayment")
.addField("id", String::class.java, FieldAttribute.PRIMARY_KEY)
.addField("typeValue", Int::class.java)
.addField("statusValue", Int::class.java)
.addField("value", Double::class.java)
.addField("referenceNumber", String::class.java)
schema.get("Order")!!
.addField("cashPaymentAmount", Float::class.java)
.addField("change", Float::class.java)
oldVersion++
}
}
}
App.kt
class App: Application() {
override fun onCreate() {
super.onCreate()
Realm.init(this)
val realmConfig = RealmConfiguration.Builder()
.schemaVersion(2)
.migration(Migration())
.build()
Realm.getInstance(realmConfig)
Realm.setDefaultConfiguration(realmConfig)
Fresco.initialize(this)
}
}
.addField("id", String::class.java, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED)
did the trick.
if you declare the variable to be lateinit, make sure to add FieldAttribute.REQUIRED.
Basically you are adding a new field "id" which is primary key (hence required key).
If you do not specify any value while initialisation (lateinit), how will realm migrate all the earlier records, which doesn't have an id, but is required after migration ? Hence the error.
Below solutions might work
Either pre-populate the id's (without using lateinit)
Transform your earlier records to have id's if they don't have
Check the official examples here
For me, it happened after I have done the Migration.
I have made a non-nullable object in Kotlin and on migration I was creating a nullable wrapper type like Double, Int, etc.
Just use
Double::class.java
instead of
Double::class.javaObjectType
I need to have two different schema in one app using realm db.
It seems that should work below solution:
open class AModel : RealmObject() {
var a: Int = 0
}
open class BModel : RealmObject() {
var b: Int = 0
}
open class XModel : RealmObject() {
var x: Int = 0
}
open class YModel : RealmObject() {
var y: Int = 0
}
#RealmModule(classes = [AModel::class,BModel::class])
open class Schema1
val conf1 = new RealmConfiguration.Builder()
.name("db1.realm")
.schemaVersion(1)
.modules(Schema1())
.build();
#RealmModule(classes = [XModel::class,YModel::class])
open class Schema2
val conf2 = new RealmConfiguration.Builder()
.name("db2.realm")
.schemaVersion(2)
.modules(Schema2())
.build();
but when app starts and calls Realm.setDefaultConfiguration(conf1), it prints below error:
com.example.XModel is not part of the schema for this Realm
So it seems that I miss something in configuration but looking at docs I cannot figure out what. So what I miss?
The problem is that you set the schema version of conf2 as a second version, but the first one was never created. Change it for this:
...
val conf2 = new RealmConfiguration.Builder()
.name("db2.realm")
.schemaVersion(1)
.modules(Schema2())
.build();
I am doing some reporting kind of things using customer query. for that, i have to fetch the data from respective node's database.but,no clue how to do that.its normally fetching all data irrespective of node.
Let's do an example based on the IOU CorDapp (https://github.com/corda/cordapp-example/). There are several ways you can do this:
1. Via an API endpoint
This endpoint will return any IOUs stored on the node with a value above minValue:
#GET
#Path("ious-above-value")
#Produces(MediaType.APPLICATION_JSON)
fun getIOUsAboveValue(#QueryParam("minValue") minValue: Int): List<IOUState> {
val results = builder {
val currencyIndex = IOUSchemaV1.PersistentIOU::value.greaterThan(minValue)
val customCriteria = QueryCriteria.VaultCustomQueryCriteria(currencyIndex)
rpcOps.vaultQueryBy<IOUState>(customCriteria)
}
val stateAndRefs = results.states
return stateAndRefs.map { stateAndRef -> stateAndRef.state.data }
}
2. Via a client
This client will return any IOUs stored on the node with a value above minValue:
fun main(args: Array<String>) {
require(args.size == 1) { "Usage: ExampleClientRPC <node address>" }
val nodeAddress = NetworkHostAndPort.parse(args[0])
val client = CordaRPCClient(nodeAddress)
// Can be amended in the com.example.MainKt file.
val proxy = client.start("user1", "test").proxy
val results = builder {
val currencyIndex = IOUSchemaV1.PersistentIOU::value.greaterThan(3)
val customCriteria = QueryCriteria.VaultCustomQueryCriteria(currencyIndex)
proxy.vaultQueryBy<IOUState>(customCriteria)
}
val stateAndRefs = results.states
val states = stateAndRefs.map { stateAndRef -> stateAndRef.state.data }
}
3. Via the node's database directly
You can log into the node's database by following the instructions here: https://docs.corda.net/node-database.html. You will then be able to execute SQL queries against the node's database directly.
I need to know when the new state got committed into the node's vault for getting the timestamp at that moment. So, I think if I can handle the committed state then trigger my timestamp record class would be nice.
By the way, please let me know if you have any suggestions about capturing timestamp on state evolving over time.
Yes, you can do this using the CordaRPCOps.vaultTrackBy method. This method returns an observable of updates to the vault. You can subscribe to this observable to be notified whenever a new state is recorded in the vault.
RPC example
fun main(args: Array<String>) {
require(args.size == 1) { "Usage: ExampleClientRPC <node address>" }
val nodeAddress = NetworkHostAndPort.parse(args[0])
val client = CordaRPCClient(nodeAddress)
val rpcOps = client.start("user1", "test").proxy
val updates = rpcOps.vaultTrackBy<ContractState>().updates
// Log the 'placed' IOU states and listen for new ones.
updates.toBlocking().subscribe { update ->
update.produced.forEach { stateAndRef ->
val timeStamp = Instant.now()
// TODO("Use the timestamp as you wish.")
}
}
}
Flow test example
class FlowTests {
lateinit var network: MockNetwork
lateinit var a: StartedMockNode
lateinit var b: StartedMockNode
#Before
fun setup() {
network = MockNetwork(
listOf("com.example.contract"),
// We need each node to operate on a separate thread so that we can
// subscribe to the vault updates on a separate thread later.
threadPerNode = true)
a = network.createPartyNode()
b = network.createPartyNode()
listOf(a, b).forEach { it.registerInitiatedFlow(ExampleFlow.Acceptor::class.java) }
}
#After
fun tearDown() {
network.stopNodes()
}
#Test
fun `flowTest`() {
// Trying to access the node database in a separate thread would result in a
// `java.lang.IllegalStateException: Was expecting to find CordaPersistence set on current thread` exception
val updates = a.services.vaultService.trackBy<ContractState>().updates
Thread {
updates.toBlocking().subscribe { update ->
update.produced.forEach { stateAndRef ->
val timeStamp = Instant.now()
// TODO("Use the timestamp as you wish.")
}
}
}.start()
repeat(3) {
val flow = ExampleFlow.Initiator(1, b.info.singleIdentity())
a.startFlow(flow).getOrThrow()
}
// We give the other thread time to observe the updates.
Thread.sleep(10000)
}
}