Skip to content

Commit

Permalink
Faux block mode (#55)
Browse files Browse the repository at this point in the history
* first pass at faux block mode for AbstractPbClient
* block mode is going to be deprecated and unsupported by the chain soon, so this is an estimation of the current behavior of submitting in block mode
* coroutine client faux block implementation
* remove commented out code
* switch back to blocking calls in AbstractPbClient
* Test for SYNC mode
* Add txHashHandler to coroutine client for feature parity
  • Loading branch information
celloman authored Oct 13, 2023
1 parent d784c46 commit 0acbe8d
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 34 deletions.
1 change: 1 addition & 0 deletions client-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
libs.grpc.alts,
libs.grpc.netty,
libs.provenance.protos,
libs.figuretech.hdwallet,
).forEach(::api)

testImplementation(libs.kotlin.test.junit)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.provenance.client.common.exceptions

import io.grpc.Status
import io.grpc.StatusRuntimeException

class TransactionTimeoutException(message: String): StatusRuntimeException(Status.DEADLINE_EXCEEDED.withDescription(message))
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.provenance.client.common.extensions

import cosmos.tx.v1beta1.TxOuterClass
import tech.figure.hdwallet.common.hashing.sha256
import tech.figure.hdwallet.encoding.base16.Base16

fun TxOuterClass.TxRaw.txHash(): String = toByteArray().sha256().toHexString()
fun ByteArray.toHexString(): String = Base16.encode(this).uppercase()
1 change: 1 addition & 0 deletions client-coroutines/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
libs.grpc.netty,
libs.kotlinx.coroutines,
libs.provenance.protos,
libs.protobuf.kotlin,
).forEach(::api)

listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,30 @@ package io.provenance.client.coroutines
import com.google.protobuf.ByteString
import cosmos.auth.v1beta1.QueryGrpcKt
import cosmos.auth.v1beta1.QueryOuterClass
import cosmos.tx.v1beta1.ServiceOuterClass
import cosmos.base.tendermint.v1beta1.getLatestBlockRequest
import cosmos.tx.v1beta1.ServiceOuterClass.BroadcastMode
import cosmos.tx.v1beta1.ServiceOuterClass.BroadcastTxRequest
import cosmos.tx.v1beta1.ServiceOuterClass.BroadcastTxResponse
import cosmos.tx.v1beta1.TxOuterClass
import cosmos.tx.v1beta1.TxOuterClass.TxBody
import cosmos.tx.v1beta1.getTxRequest
import cosmos.tx.v1beta1.tx
import io.grpc.StatusException
import io.grpc.StatusRuntimeException
import io.grpc.netty.NettyChannelBuilder
import io.provenance.client.common.exceptions.TransactionTimeoutException
import io.provenance.client.common.extensions.txHash
import io.provenance.client.common.gas.GasEstimate
import io.provenance.client.grpc.BaseReq
import io.provenance.client.grpc.BaseReqSigner
import io.provenance.client.grpc.ChannelOpts
import io.provenance.client.grpc.grpcChannel
import kotlinx.coroutines.delay
import java.io.Closeable
import java.net.URI
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.milliseconds


open class PbCoroutinesClient(
Expand Down Expand Up @@ -87,18 +98,15 @@ open class PbCoroutinesClient(
)
}

private fun mkTx(body: TxOuterClass.Tx.Builder.() -> Unit): TxOuterClass.Tx =
TxOuterClass.Tx.newBuilder().also(body).build()

suspend fun estimateTx(baseReq: BaseReq): GasEstimate {
val tx = mkTx {
val transaction = tx {
body = baseReq.body
authInfo = baseReq.buildAuthInfo()
signaturesList.add(ByteString.EMPTY)
signatures.add(ByteString.EMPTY)
}

val gasAdjustment = baseReq.gasAdjustment ?: GasEstimate.DEFAULT_FEE_ADJUSTMENT
return gasEstimationMethod(this, tx, gasAdjustment)
return gasEstimationMethod(this, transaction, gasAdjustment)
}

private fun buildTx(
Expand All @@ -122,31 +130,72 @@ open class PbCoroutinesClient(
suspend fun broadcastTx(
baseReq: BaseReq,
gasEstimate: GasEstimate,
mode: ServiceOuterClass.BroadcastMode = ServiceOuterClass.BroadcastMode.BROADCAST_MODE_SYNC
): ServiceOuterClass.BroadcastTxResponse {
return cosmosService.broadcastTx(
ServiceOuterClass.BroadcastTxRequest
.newBuilder()
.setTxBytes(buildTx(baseReq, gasEstimate).toByteString())
.setMode(mode)
.build()
)
mode: BroadcastMode = BroadcastMode.BROADCAST_MODE_SYNC,
txHashHandler: PreBroadcastTxHashHandler? = null,
): BroadcastTxResponse {
return buildTx(baseReq, gasEstimate)
.also { txRaw ->
txHashHandler?.let { it(txRaw.txHash()) }
}
.emulateBlockMode(mode, baseReq.body.timeoutHeight) {
cosmosService.broadcastTx(it)
}
}

suspend fun estimateAndBroadcastTx(
txBody: TxBody,
signers: List<BaseReqSigner>,
mode: ServiceOuterClass.BroadcastMode = ServiceOuterClass.BroadcastMode.BROADCAST_MODE_SYNC,
mode: BroadcastMode = BroadcastMode.BROADCAST_MODE_SYNC,
gasAdjustment: Double? = null,
feeGranter: String? = null,
feePayer: String? = null,
): ServiceOuterClass.BroadcastTxResponse = baseRequest(
txHashHandler: PreBroadcastTxHashHandler? = null,
): BroadcastTxResponse = baseRequest(
txBody = txBody,
signers = signers,
gasAdjustment = gasAdjustment,
feeGranter = feeGranter,
feePayer = feePayer,
).let { baseReq -> broadcastTx(baseReq, estimateTx(baseReq), mode) }
).let { baseReq -> broadcastTx(baseReq, estimateTx(baseReq), mode, txHashHandler) }

private suspend fun TxOuterClass.TxRaw.emulateBlockMode(
mode: BroadcastMode,
providedTimeoutHeight: Long,
handler: suspend (BroadcastTxRequest) -> BroadcastTxResponse
): BroadcastTxResponse {
val (actualMode, simulateBlock) = if (mode == BroadcastMode.BROADCAST_MODE_BLOCK) {
BroadcastMode.BROADCAST_MODE_SYNC to true
} else {
mode to false
}
return handler(BroadcastTxRequest.newBuilder()
.setTxBytes(this.toByteString())
.setMode(actualMode)
.build()
).let { res ->
if (simulateBlock) {
val timeoutHeight = providedTimeoutHeight.takeIf { it > 0 } ?: (latestHeight() + 10) // default to 10 block timeout for polling if no height set
val txHash = res.txResponse.txhash
do {
try {
val tx = cosmosService.getTx(getTxRequest { hash = txHash })
return res.toBuilder()
.setTxResponse(tx.txResponse)
.build()
} catch (e: StatusException) {
if (e.message?.contains("not found") == true) {
delay(1000.milliseconds)
continue
}
throw e
}
} while (latestHeight() <= timeoutHeight)
throw TransactionTimeoutException("Failed to complete transaction with hash $txHash by height $timeoutHeight")
} else res
}
}

suspend fun latestHeight() = this.tendermintService.getLatestBlock(getLatestBlockRequest { }).block.header.height
}

/**
Expand All @@ -164,3 +213,5 @@ suspend fun QueryGrpcKt.QueryCoroutineStub.getBaseAccount(bech32Address: String)
else -> throw IllegalArgumentException("Account type not handled:$typeUrl")
}
}

typealias PreBroadcastTxHashHandler = suspend (String) -> Unit
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
package io.provenance.client.coroutines

import cosmos.auth.v1beta1.QueryOuterClass
import cosmos.bank.v1beta1.Tx
import cosmos.base.v1beta1.coin
import cosmos.tx.v1beta1.ServiceOuterClass
import cosmos.tx.v1beta1.TxOuterClass
import cosmos.tx.v1beta1.getTxRequest
import io.grpc.StatusException
import io.grpc.StatusRuntimeException
import io.provenance.client.grpc.BaseReqSigner
import io.provenance.client.protobuf.extensions.getTx
import io.provenance.client.protobuf.extensions.toAny
import io.provenance.client.wallet.WalletSigner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Ignore
import tech.figure.hdwallet.bip39.MnemonicWords
import tech.figure.hdwallet.hrp.Hrp
import tech.figure.hdwallet.wallet.Wallet
import java.net.URI
import kotlin.test.Test
import kotlin.test.assertTrue

@OptIn(ExperimentalCoroutinesApi::class)
@Ignore
class PbClientTest {
val pbClient = PbCoroutinesClient(
Expand All @@ -27,4 +43,73 @@ class PbClientTest {
)
}
}

@Test
fun `Simulated BROADCAST_MODE_BLOCK works`() = runTest {
val signer = testWalletSigner()
val result = pbClient.estimateAndBroadcastTx(
TxOuterClass.TxBody.newBuilder().addMessages(
Tx.MsgSend.newBuilder()
.setFromAddress(signer.address())
.setToAddress(signer.address())
.addAmount(coin {
this.amount = "1"
this.denom = "nhash"
}).build()
.toAny()).build(),
listOf(BaseReqSigner(signer)),
mode = ServiceOuterClass.BroadcastMode.BROADCAST_MODE_BLOCK,
)

assert(result.txResponse.height > 0) { "Transaction response had no height" }
assert(result.txResponse.code == 0) { "Transaction not successful" }
}

@Test
fun `Simulated BROADCAST_MODE_SYNC works`() = runTest {
val signer = testWalletSigner()
var preHash: String? = null
val result = pbClient.estimateAndBroadcastTx(
TxOuterClass.TxBody.newBuilder().addMessages(
Tx.MsgSend.newBuilder()
.setFromAddress(signer.address())
.setToAddress(signer.address())
.addAmount(coin {
this.amount = "1"
this.denom = "nhash"
}).build()
.toAny()).build(),
listOf(BaseReqSigner(signer)),
mode = ServiceOuterClass.BroadcastMode.BROADCAST_MODE_SYNC,
txHashHandler = { preHash = it }
)

assert(preHash != null) { "preHash not received" }
assert(result.txResponse.txhash == preHash) { "Transaction response had no txHash" }
var tx: ServiceOuterClass.GetTxResponse? = null
for (i in 1..6) {
try {
tx = pbClient.cosmosService.getTx(getTxRequest { hash = preHash!! })
} catch (e: StatusException) {
if (e.message?.contains("not found") == true) {
Thread.sleep(1000)
continue
}
throw e
}
}
assert(tx != null) { "Transaction $preHash not fetched" }
assert(tx!!.txResponse.height > 0) { "Transaction response had no height" }
assert(tx.txResponse.code == 0) { "Transaction not successful" }
}

private fun testWalletSigner(): WalletSigner =
MnemonicWords.generate().let {
Wallet.fromMnemonic(
hrp = Hrp.ProvenanceBlockchain.testnet,
passphrase = "",
mnemonicWords = it,
testnet = true
)
}.let { WalletSigner(it["m/44'/1'/0'/0'/0'"]) }
}
Loading

0 comments on commit 0acbe8d

Please sign in to comment.