Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uniffi demo holder #1058

Merged
merged 8 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions uniffi_aries_vcx/core/src/core/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::Arc;

use aries_vcx::{
aries_vcx_core::{
anoncreds::credx_anoncreds::IndyCredxAnonCreds,
anoncreds::{base_anoncreds::BaseAnonCreds, credx_anoncreds::IndyCredxAnonCreds},
ledger::{
base_ledger::TxnAuthrAgrmtOptions,
indy_vdr_ledger::{indyvdr_build_ledger_read, IndyVdrLedgerRead},
Expand Down Expand Up @@ -55,15 +55,23 @@ pub fn new_indy_profile(
) -> VcxUniFFIResult<Arc<ProfileHolder>> {
block_on(async {
let wh = create_and_open_wallet(&wallet_config).await?;
let wallet = IndySdkWallet::new(wh);

let anoncreds = IndyCredxAnonCreds;

anoncreds
.prover_create_link_secret(&wallet, "main")
.await
.ok();

let indy_vdr_config = PoolConfig::default();
let cache_config = InMemoryResponseCacherConfig::builder()
.ttl(std::time::Duration::from_secs(60))
.capacity(1000)?
.build();
let ledger_pool = IndyVdrLedgerPool::new(genesis_file_path, indy_vdr_config, vec![])?;
let request_submitter = IndyVdrSubmitter::new(ledger_pool);
let ledger_read = indyvdr_build_ledger_read(request_submitter, cache_config)?;
let wallet = IndySdkWallet::new(wh);
let ledger_read = indyvdr_build_ledger_read(request_submitter.clone(), cache_config)?;
let profile = UniffiProfile {
anoncreds: IndyCredxAnonCreds,
wallet,
Expand Down
4 changes: 2 additions & 2 deletions uniffi_aries_vcx/core/src/handlers/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,15 @@ impl Connection {

pub fn send_message(
&self,
profile_holder: Arc<ProfileHolder>,
profile: Arc<ProfileHolder>,
message: String,
) -> VcxUniFFIResult<()> {
let message = serde_json::from_str(&message)?;
let handler = self.handler.lock()?.clone();

block_on(async {
handler
.send_message(profile_holder.inner.wallet(), &message, &HttpClient)
.send_message(profile.inner.wallet(), &message, &HttpClient)
.await?;
Ok(())
})
Expand Down
8 changes: 4 additions & 4 deletions uniffi_aries_vcx/core/src/handlers/holder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::{Arc, Mutex};

use aries_vcx::{
handlers::issuance::holder::Holder as VcxHolder,
handlers::issuance::holder::Holder as VcxHolder, messages::AriesMessage,
protocols::issuance::holder::state_machine::HolderState as VcxHolderState,
};

Expand Down Expand Up @@ -97,9 +97,9 @@ impl Holder {
pub fn get_msg_credential_request(&self) -> VcxUniFFIResult<String> {
let handler = self.handler.lock()?;

Ok(serde_json::to_string(
&handler.clone().get_msg_credential_request()?,
)?)
Ok(serde_json::to_string(&AriesMessage::from(
handler.clone().get_msg_credential_request()?,
))?)
}

pub fn decline_offer(&self, comment: Option<String>) -> VcxUniFFIResult<String> {
Expand Down
3 changes: 3 additions & 0 deletions uniffi_aries_vcx/core/src/vcx.udl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ interface Connection {

[Throws=VcxUniFFIError]
void send_ack(ProfileHolder profile);

[Throws=VcxUniFFIError]
void send_message(ProfileHolder profile, string message);
};

interface Holder {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.hyperledger.ariesvcx

import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -18,21 +22,28 @@ import org.hyperledger.ariesvcx.utils.await
data class AppUiState(
val profileReady: Boolean = false,
val connectionInvitationReceived: Boolean = false,
val connectionCompleted: Boolean = false
val connectionCompleted: Boolean = false,
val offerReceived: Boolean = false
)

class AppDemoController : ViewModel() {
private val httpClient = OkHttpClient()

private var profile: ProfileHolder? = null
private var connection: Connection? = null
private var holder: Holder? = null

private var onConnectionComplete: (connection: Connection) -> Unit = {}
private var onOfferReceived: () -> Unit = {}

// Expose screen UI state
private val _state = MutableStateFlow(AppUiState())
val states: StateFlow<AppUiState> = _state.asStateFlow()

fun getHolder (): Holder? {
return holder
}

private val walletConfig = WalletConfig(
walletName = "test_create_wallet_add_uuid_here",
walletKey = "8dvfYSt5d1taSd6yJdpjq4emkwsPDDLYxkNFysFD2cZY",
Expand Down Expand Up @@ -88,7 +99,7 @@ class AppDemoController : ViewModel() {
val relayResponse = httpClient.newCall(pollRelayRequest).await()
if (relayResponse.code == 200) {
val message = relayResponse.body!!.string()

Log.d("MESSAGE", "awaitConnectionCompletion: $message")
val unpackedMessage = unpackMessage(
profile!!,
message
Expand All @@ -101,7 +112,7 @@ class AppDemoController : ViewModel() {
Log.d("AppDemoController", "connection state: ${connection!!.getState()}")

_state.update { it.copy(connectionCompleted = true) }
onConnectionComplete(connection!!)
onConnectionComplete.invoke(connection!!)
break
}
}
Expand All @@ -110,4 +121,46 @@ class AppDemoController : ViewModel() {
fun subscribeToConnectionComplete(onComplete: (connection: Connection) -> Unit) {
onConnectionComplete = onComplete
}

suspend fun processOfferRequest() {
withContext(Dispatchers.IO) {
holder?.prepareCredentialRequest(profile!!, "4xE68b6S5VRFrKMMG1U95M")
Log.d("HOLDER", "processOfferRequest: ${holder?.getState()}")
val message = holder?.getMsgCredentialRequest()
connection?.sendMessage(profile!!, message!!)
}
}

suspend fun awaitCredentialPolling() {
val pollRelayRequest = Request.Builder()
.url("$BASE_RELAY_ENDPOINT/pop_user_message/$RELAY_USER_ID")
.build()
while(true) {
delay(2000)
val relayResponse = httpClient.newCall(pollRelayRequest).await()
if (relayResponse.code == 200) {
val message = relayResponse.body!!.string()

val unpackedMessage = unpackMessage(
profile!!,
message
)

Log.d("MESSAGE", "$unpackedMessage.message")

if (!_state.value.offerReceived) {
Log.d("OFFER", "awaitCredentialPolling: received OFFER")
holder = createFromOffer("", unpackedMessage.message)

_state.update { it.copy(offerReceived = true) }
onOfferReceived.invoke()
} else {
Log.d("CREDENTIAL", "awaitCredentialPolling: received CREDENTIAL")
holder?.processCredential(profile!!, unpackedMessage.message)

_state.update { it.copy(offerReceived = false) }
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package org.hyperledger.ariesvcx

// Set your public IP address here, this endpoint will be used while communicating with the peer(agent).
const val BASE_RELAY_ENDPOINT = "https://b199-27-57-116-96.ngrok-free.app";
const val BASE_RELAY_ENDPOINT = "https://8dfb-2401-4900-8039-4a51-1e9a-23be-5e1b-f103.ngrok-free.app";
const val RELAY_USER_ID = "demo-user-1";
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.hyperledger.ariesvcx

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import kotlinx.coroutines.launch

@Composable
fun HolderScreen(
demoController: AppDemoController,
navController: NavHostController,
) {
val demoState by demoController.states.collectAsState()
val scope = rememberCoroutineScope()

LaunchedEffect(Unit) {
demoController.awaitCredentialPolling()
}

if (demoState.offerReceived) {
AlertDialog(
onDismissRequest = { },
title = { Text("Accept this credential?") },
text = { Text(demoController.getHolder()?.getAttributes()!!) },
confirmButton = {
TextButton(onClick = {
scope.launch {
demoController.processOfferRequest()
}
}) {
Text("Accept")
}
},
dismissButton = {
TextButton(onClick = { }) {
Text("Cancel")
}
},
)
}

Column(
modifier = Modifier
.fillMaxSize()
.padding(50.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "Waiting for credential offer")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
Expand Down Expand Up @@ -34,13 +37,16 @@ fun HomeScreen(
val scope = rememberCoroutineScope()
val context = LocalContext.current
val file = prepareGenesisFile(context)
demoController.subscribeToConnectionComplete { newConn ->
scope.launch(Dispatchers.Main) {
Toast.makeText(
context,
"New Connection Created",
Toast.LENGTH_SHORT
).show()

LaunchedEffect(key1 = Unit) {
demoController.subscribeToConnectionComplete {
scope.launch(Dispatchers.Main) {
Toast.makeText(
context,
"New Connection Created",
Toast.LENGTH_LONG
).show()
}
}
}

Expand Down Expand Up @@ -73,6 +79,7 @@ fun HomeScreen(
}
Button(enabled = (demoState.connectionCompleted),
onClick = {
navController.navigate(Destination.Holder.route)
}) {
Text(text = "Receive a credential")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,18 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import okhttp3.OkHttpClient
import org.hyperledger.ariesvcx.ui.theme.DemoTheme


sealed class Destination(val route: String) {
object Home : Destination("home")
object QRScan : Destination("scan")
object Holder : Destination("holder")
}

class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -65,5 +62,12 @@ fun NavigationAppHost(
navController = navController,
)
}

composable(Destination.Holder.route) {
HolderScreen(
demoController = demoController,
navController = navController,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ fun ScanScreen(

scannedQRCodeText?.let { text ->
val encoded = Uri.parse(text)?.getQueryParameter("c_i")
if (encoded == null) {
scannedQRCodeText = null
return@let
}
val decoded = String(Base64.decode(encoded, Base64.DEFAULT))

AlertDialog(
Expand Down Expand Up @@ -100,6 +104,7 @@ fun ScanScreen(
LaunchedEffect(key1 = true) {
launcher.launch(Manifest.permission.CAMERA)
}

Column(
modifier = Modifier
.fillMaxSize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import java.io.FileWriter
fun prepareGenesisFile(context: Context): File {
val file = File(context.filesDir, "genesis")
if (!file.exists()) {
val transactions = context.resources.openRawResource(R.raw.transactions).bufferedReader()
val transactions = context.resources.openRawResource(R.raw.bcovrin_transactions).bufferedReader()
.use { it.readText() }
val bufferedWriter = BufferedWriter(FileWriter(file))
bufferedWriter.write(transactions)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"127.0.0.1","client_port":9702,"node_ip":"127.0.0.1","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"127.0.0.1","client_port":9704,"node_ip":"127.0.0.1","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"127.0.0.1","client_port":9706,"node_ip":"127.0.0.1","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"127.0.0.1","client_port":9708,"node_ip":"127.0.0.1","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"138.197.138.255","client_port":9704,"node_ip":"138.197.138.255","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"138.197.138.255","client_port":9706,"node_ip":"138.197.138.255","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"}
{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"138.197.138.255","client_port":9708,"node_ip":"138.197.138.255","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}
Loading