From d4c04b1fb8e82c6ada57c7d4def0213381454325 Mon Sep 17 00:00:00 2001 From: Swapnil Tripathi Date: Sat, 11 Nov 2023 21:43:48 +0530 Subject: [PATCH] add holder api to demo app --- .../src/crypto/base64/rust_base64.rs | 12 +++- .../core/src/handlers/connection.rs | 4 +- uniffi_aries_vcx/core/src/vcx.udl | 3 + .../hyperledger/ariesvcx/AppDemoController.kt | 52 +++++++++++++++- .../org/hyperledger/ariesvcx/Constants.kt | 2 +- .../org/hyperledger/ariesvcx/HolderScreen.kt | 62 +++++++++++++++++++ .../org/hyperledger/ariesvcx/HomeScreen.kt | 22 ++++--- .../org/hyperledger/ariesvcx/ScanScreen.kt | 1 + .../demo/app/src/main/res/raw/transactions | 8 +-- 9 files changed, 145 insertions(+), 21 deletions(-) diff --git a/libvdrtools/indy-utils/src/crypto/base64/rust_base64.rs b/libvdrtools/indy-utils/src/crypto/base64/rust_base64.rs index 1c48f23779..be645f1df7 100644 --- a/libvdrtools/indy-utils/src/crypto/base64/rust_base64.rs +++ b/libvdrtools/indy-utils/src/crypto/base64/rust_base64.rs @@ -1,6 +1,12 @@ -use base64::{engine::general_purpose, Engine}; +use base64::{ + alphabet, + engine::{general_purpose, DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, + Engine, +}; use indy_api_types::errors::prelude::*; - +const ANY_PADDING: GeneralPurposeConfig = + GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent); +const URL_SAFE_ANY_PADDING: GeneralPurpose = GeneralPurpose::new(&alphabet::URL_SAFE, ANY_PADDING); pub fn encode(doc: &[u8]) -> String { general_purpose::STANDARD.encode(doc) } @@ -16,7 +22,7 @@ pub fn encode_urlsafe(doc: &[u8]) -> String { } pub fn decode_urlsafe(doc: &str) -> Result, IndyError> { - general_purpose::URL_SAFE.decode(doc).map_err(|e| { + URL_SAFE_ANY_PADDING.decode(doc).map_err(|e| { e.to_indy( IndyErrorKind::InvalidStructure, "Invalid base64URL_SAFE sequence", diff --git a/uniffi_aries_vcx/core/src/handlers/connection.rs b/uniffi_aries_vcx/core/src/handlers/connection.rs index e6d5ab6c79..b77787399a 100644 --- a/uniffi_aries_vcx/core/src/handlers/connection.rs +++ b/uniffi_aries_vcx/core/src/handlers/connection.rs @@ -240,7 +240,7 @@ impl Connection { pub fn send_message( &self, - profile_holder: Arc, + profile: Arc, message: String, ) -> VcxUniFFIResult<()> { let message = serde_json::from_str(&message)?; @@ -248,7 +248,7 @@ impl Connection { block_on(async { handler - .send_message(profile_holder.inner.wallet(), &message, &HttpClient) + .send_message(profile.inner.wallet(), &message, &HttpClient) .await?; Ok(()) }) diff --git a/uniffi_aries_vcx/core/src/vcx.udl b/uniffi_aries_vcx/core/src/vcx.udl index 001bfbfbb4..f8db410801 100644 --- a/uniffi_aries_vcx/core/src/vcx.udl +++ b/uniffi_aries_vcx/core/src/vcx.udl @@ -66,6 +66,9 @@ interface Connection { [Throws=VcxUniFFIError] void send_ack(ProfileHolder profile); + + [Throws=VcxUniFFIError] + void send_message(ProfileHolder profile, string message); }; interface Holder { diff --git a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/AppDemoController.kt b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/AppDemoController.kt index ad0d53c4fb..09540f9808 100644 --- a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/AppDemoController.kt +++ b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/AppDemoController.kt @@ -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 @@ -18,7 +22,8 @@ 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() { @@ -26,8 +31,10 @@ class AppDemoController : ViewModel() { 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()) @@ -88,7 +95,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 @@ -101,7 +108,7 @@ class AppDemoController : ViewModel() { Log.d("AppDemoController", "connection state: ${connection!!.getState()}") _state.update { it.copy(connectionCompleted = true) } - onConnectionComplete(connection!!) + onConnectionComplete.invoke(connection!!) break } } @@ -110,4 +117,43 @@ class AppDemoController : ViewModel() { fun subscribeToConnectionComplete(onComplete: (connection: Connection) -> Unit) { onConnectionComplete = onComplete } + + fun subscribeToShowDialog(onShowDialog: () -> Unit) { + onOfferReceived = onShowDialog + } + + fun processOfferRequest() { + holder?.prepareCredentialRequest(profile!!, "4xE68b6S5VRFrKMMG1U95M") + 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 + ) + + if (holder == null) { + Log.d("OFFER", "awaitCredentialPolling: received offer") + holder = createFromOffer("", unpackedMessage.message) + _state.update { it.copy(offerReceived = true) } + onOfferReceived.invoke() + processOfferRequest() + } else { + Log.d("CREDENTIAL", "awaitCredentialPolling: received credential") + holder?.processCredential(profile!!, unpackedMessage.message) + } + } + } + } } diff --git a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/Constants.kt b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/Constants.kt index 90fbb96fb3..0e63e94bf9 100644 --- a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/Constants.kt +++ b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/Constants.kt @@ -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://7365-223-236-178-57.ngrok-free.app"; const val RELAY_USER_ID = "demo-user-1"; diff --git a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HolderScreen.kt b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HolderScreen.kt index a1d54c55d1..4c301d2c27 100644 --- a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HolderScreen.kt +++ b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HolderScreen.kt @@ -1,21 +1,83 @@ package org.hyperledger.ariesvcx +import android.net.Uri +import android.util.Base64 +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.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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue 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.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable fun HolderScreen( demoController: AppDemoController, navController: NavHostController, ) { + val demoState by demoController.states.collectAsState() + val scope = rememberCoroutineScope() + val context = LocalContext.current + + var credentialOffer by remember { + mutableStateOf(null) + } + + credentialOffer?.let { + AlertDialog( + onDismissRequest = { credentialOffer = null }, + title = { Text("Accept this invitation?") }, + text = { Text(credentialOffer!!) }, + confirmButton = { + TextButton(onClick = { + scope.launch { + demoController.processOfferRequest() + } + }) { + Text("Accept") + } + }, + dismissButton = { + TextButton(onClick = { credentialOffer = null }) { + Text("Cancel") + } + }, + ) + } + + LaunchedEffect(Unit) { + demoController.awaitCredentialPolling() + } + + LaunchedEffect(Unit) { + demoController.subscribeToShowDialog { + scope.launch(Dispatchers.Main) { + Toast.makeText( + context, + "New Offer Received", + Toast.LENGTH_LONG + ).show() + } + } + } + Column( modifier = Modifier .fillMaxSize() diff --git a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HomeScreen.kt b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HomeScreen.kt index 257006aa4d..251b777107 100644 --- a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HomeScreen.kt +++ b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/HomeScreen.kt @@ -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 @@ -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() + } } } @@ -71,7 +77,7 @@ fun HomeScreen( }) { Text(text = "Scan QR Code") } - Button(enabled = /*(demoState.connectionCompleted)*/ true, + Button(enabled = (demoState.connectionCompleted), onClick = { navController.navigate(Destination.Holder.route) }) { diff --git a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/ScanScreen.kt b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/ScanScreen.kt index 8d96037473..d816927f5b 100644 --- a/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/ScanScreen.kt +++ b/uniffi_aries_vcx/demo/app/src/main/java/org/hyperledger/ariesvcx/ScanScreen.kt @@ -100,6 +100,7 @@ fun ScanScreen( LaunchedEffect(key1 = true) { launcher.launch(Manifest.permission.CAMERA) } + Column( modifier = Modifier .fillMaxSize() diff --git a/uniffi_aries_vcx/demo/app/src/main/res/raw/transactions b/uniffi_aries_vcx/demo/app/src/main/res/raw/transactions index 4dd9dd4847..0f1e4fa10d 100644 --- a/uniffi_aries_vcx/demo/app/src/main/res/raw/transactions +++ b/uniffi_aries_vcx/demo/app/src/main/res/raw/transactions @@ -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"}