Skip to content

Commit

Permalink
feat: Implement txn execution, simulation, and signing; Get programma…
Browse files Browse the repository at this point in the history
…ble txn construction working

- Added support for executing transactions on-chain.
- Added txn simulation using the `dryRunTransactionBlock` function to validate transactions without committing them to the blockchain.
- Implemented txn signing to authorize and securely submit txns.
- Got programmable txn construction working. Split, MoveCall and Transfer commands working..
  • Loading branch information
astinz committed Aug 26, 2024
1 parent e8c85f5 commit d09b090
Show file tree
Hide file tree
Showing 24 changed files with 525 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,16 @@ actual fun importFromMnemonic(mnemonic: List<String>): KeyPair {
val seed = MnemonicCode.toSeed(mnemonic, "")
return generateKeyPair(seed, SignatureScheme.ED25519)
}

actual fun sign(message: ByteArray, privateKey: PrivateKey): ByteArray {
when (privateKey) {
is Ed25519PrivateKey -> {
val signer = org.bouncycastle.crypto.signers.Ed25519Signer()
val privateKeyParameters = Ed25519PrivateKeyParameters(privateKey.data, 0)
signer.init(true, privateKeyParameters)
signer.update(message, 0, message.size)
return signer.generateSignature()
}
else -> throw SignatureSchemeNotSupportedException()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ actual fun importFromMnemonic(mnemonic: String): KeyPair {
actual fun importFromMnemonic(mnemonic: List<String>): KeyPair {
TODO("Not yet implemented")
}

actual fun sign(
message: ByteArray,
privateKey: PrivateKey,
): ByteArray {
TODO("Not yet implemented")
}
2 changes: 2 additions & 0 deletions lib/src/commonMain/kotlin/xyz/mcxross/ksui/account/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ abstract class Account {

abstract val scheme: SignatureScheme

abstract fun sign(message: ByteArray): ByteArray

companion object {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class Ed25519Account(private val privateKey: Ed25519PrivateKey) : Account() {
override val scheme: SignatureScheme
get() = SignatureScheme.ED25519

override fun sign(message: ByteArray): ByteArray {
return privateKey.sign(message)
}

constructor(privateKey: Ed25519PrivateKey, mnemonic: String) : this(privateKey) {
this.mnemonic = mnemonic
}
Expand Down
73 changes: 71 additions & 2 deletions lib/src/commonMain/kotlin/xyz/mcxross/ksui/api/Transaction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@
*/
package xyz.mcxross.ksui.api

import xyz.mcxross.ksui.account.Account
import xyz.mcxross.ksui.generated.DryRunTransactionBlock
import xyz.mcxross.ksui.generated.ExecuteTransactionBlock
import xyz.mcxross.ksui.internal.dryRunTransactionBlock
import xyz.mcxross.ksui.internal.executeTransactionBlock
import xyz.mcxross.ksui.internal.getTotalTransactionBlocks
import xyz.mcxross.ksui.internal.signAndSubmitTransaction
import xyz.mcxross.ksui.model.ExecuteTransactionBlockResponseOptions
import xyz.mcxross.ksui.model.Option
import xyz.mcxross.ksui.model.SuiConfig
import xyz.mcxross.ksui.model.TransactionBlockResponseOptions
import xyz.mcxross.ksui.model.TransactionBlocks
import xyz.mcxross.ksui.model.TransactionBlocksOptions
import xyz.mcxross.ksui.protocol.Transaction
import xyz.mcxross.ksui.ptb.ProgrammableTransaction

/**
* Transaction API implementation
Expand All @@ -31,6 +39,37 @@ import xyz.mcxross.ksui.protocol.Transaction
*/
class Transaction(val config: SuiConfig) : Transaction {

/**
* Execute a transaction block
*
* This function will execute a transaction block with the given transaction bytes and signatures.
*
* @param txnBytes The transaction bytes
* @param signatures The signatures
* @param option The options to use for response
* @return An [Option] of nullable [ExecuteTransactionBlock.Result]
*/
override suspend fun executeTransactionBlock(
txnBytes: String,
signatures: List<String>,
option: ExecuteTransactionBlockResponseOptions,
): Option.Some<ExecuteTransactionBlock.Result?> =
executeTransactionBlock(config, txnBytes, signatures, option)

/**
* Dry run a transaction block
*
* This function will dry run a transaction block with the given transaction bytes.
*
* @param txnBytes The transaction bytes
* @param option The options to use for response
* @return An [Option] of nullable [DryRunTransactionBlock.Result]
*/
override suspend fun dryRunTransactionBlock(
txnBytes: String,
option: ExecuteTransactionBlockResponseOptions,
): Option.Some<DryRunTransactionBlock.Result?> = dryRunTransactionBlock(config, txnBytes, option)

/**
* Get the total transaction blocks
*
Expand All @@ -46,7 +85,37 @@ class Transaction(val config: SuiConfig) : Transaction {
* @return An [Option] of nullable [TransactionBlocks]
*/
override suspend fun queryTransactionBlocks(
txnBlocksOptions: TransactionBlocksOptions
txnBlocksOptions: TransactionBlockResponseOptions
): Option<TransactionBlocks> =
xyz.mcxross.ksui.internal.queryTransactionBlocks(config, txnBlocksOptions)

/**
* Sign a transaction
*
* This function will sign a transaction with the given message and signer.
*
* @param message The message
* @param signer The signer
* @return The signed transaction
*/
override fun signTransaction(message: ByteArray, signer: Account): ByteArray =
xyz.mcxross.ksui.internal.signTransaction(message, signer)

/**
* Sign and execute a transaction block
*
* This function will sign and execute a transaction block with the given programmable transaction
* and signer.
*
* @param ptb The programmable transaction
* @param signer The signer
* @param gasBudget The gas budget
* @return An [Option] of nullable [ExecuteTransactionBlock.Result]
*/
override suspend fun signAndExecuteTransactionBlock(
ptb: ProgrammableTransaction,
signer: Account,
gasBudget: ULong,
): Option.Some<ExecuteTransactionBlock.Result?> =
signAndSubmitTransaction(config, ptb, signer, gasBudget)
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ class Ed25519PrivateKey(private val privateKey: ByteArray) : PrivateKey {
* @throws IllegalArgumentException If the private key is invalid.
*/
constructor(privateKey: String) : this(PrivateKey.fromEncoded(privateKey).data)

override fun sign(data: ByteArray): ByteArray {
return sign(data, this)
}
}

class Ed25519PublicKey(val data: ByteArray) : PublicKey {
class Ed25519PublicKey(override val data: ByteArray) : PublicKey {

override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ expect fun importFromMnemonic(mnemonic: String): KeyPair

expect fun importFromMnemonic(mnemonic: List<String>): KeyPair

expect fun sign(message: ByteArray, privateKey: PrivateKey): ByteArray

fun generatePrivateKey(scheme: SignatureScheme): ByteArray {
return when (scheme) {
SignatureScheme.ED25519 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ interface PrivateKey {
)
}

fun sign(data: ByteArray): ByteArray

companion object {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
*/
package xyz.mcxross.ksui.core.crypto

interface PublicKey
interface PublicKey {
val data: ByteArray
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ internal suspend fun getOwnedObjects(
showBcs = option.showBcs,
showContent = option.showContent,
showDisplay = option.showDisplay,
showType = option.showType,
showOwner = option.showOwner,
showPreviousTransaction = option.showPreviousTransaction,
showStorageRebate = option.showStorageRebate,
Expand Down
160 changes: 146 additions & 14 deletions lib/src/commonMain/kotlin/xyz/mcxross/ksui/internal/Transaction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,74 @@
*/
package xyz.mcxross.ksui.internal

import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import xyz.mcxross.bcs.Bcs
import xyz.mcxross.ksui.account.Account
import xyz.mcxross.ksui.client.getGraphqlClient
import xyz.mcxross.ksui.core.crypto.Hash
import xyz.mcxross.ksui.core.crypto.hash
import xyz.mcxross.ksui.exception.SuiException
import xyz.mcxross.ksui.generated.DryRunTransactionBlock
import xyz.mcxross.ksui.generated.ExecuteTransactionBlock
import xyz.mcxross.ksui.generated.GetTotalTransactionBlocks
import xyz.mcxross.ksui.generated.QueryTransactionBlocks
import xyz.mcxross.ksui.model.AccountAddress
import xyz.mcxross.ksui.model.Digest
import xyz.mcxross.ksui.model.ExecuteTransactionBlockResponseOptions
import xyz.mcxross.ksui.model.Intent
import xyz.mcxross.ksui.model.IntentMessage
import xyz.mcxross.ksui.model.ObjectDigest
import xyz.mcxross.ksui.model.ObjectReference
import xyz.mcxross.ksui.model.Option
import xyz.mcxross.ksui.model.Reference
import xyz.mcxross.ksui.model.SuiAddress
import xyz.mcxross.ksui.model.SuiConfig
import xyz.mcxross.ksui.model.TransactionBlockResponseOptions
import xyz.mcxross.ksui.model.TransactionBlocks
import xyz.mcxross.ksui.model.TransactionBlocksOptions
import xyz.mcxross.ksui.model.TransactionDataComposer
import xyz.mcxross.ksui.model.content
import xyz.mcxross.ksui.model.with
import xyz.mcxross.ksui.ptb.ProgrammableTransaction

suspend fun executeTransactionBlock(
config: SuiConfig,
txnBytes: String,
signatures: List<String>,
option: ExecuteTransactionBlockResponseOptions,
): Option.Some<ExecuteTransactionBlock.Result?> {
val response =
getGraphqlClient(config)
.execute<ExecuteTransactionBlock.Result>(
ExecuteTransactionBlock(
ExecuteTransactionBlock.Variables(txBytes = txnBytes, signatures = signatures)
)
)

if (response.errors != null) {
throw SuiException(response.errors.toString())
}

return Option.Some(response.data)
}

internal suspend fun dryRunTransactionBlock(
config: SuiConfig,
txnBytes: String,
option: ExecuteTransactionBlockResponseOptions,
): Option.Some<DryRunTransactionBlock.Result?> {
val response =
getGraphqlClient(config)
.execute<DryRunTransactionBlock.Result>(
DryRunTransactionBlock(DryRunTransactionBlock.Variables(txBytes = txnBytes))
)

if (response.errors != null) {
throw SuiException(response.errors.toString())
}

return Option.Some(response.data)
}

suspend fun getTotalTransactionBlocks(config: SuiConfig): Option<Long?> {

Expand All @@ -42,25 +102,25 @@ suspend fun getTotalTransactionBlocks(config: SuiConfig): Option<Long?> {

suspend fun queryTransactionBlocks(
config: SuiConfig,
transactionBlocksOptions: TransactionBlocksOptions,
transactionBlockResponseOptions: TransactionBlockResponseOptions,
): Option<TransactionBlocks> {
val response =
getGraphqlClient(config)
.execute<QueryTransactionBlocks.Result>(
QueryTransactionBlocks(
QueryTransactionBlocks.Variables(
first = transactionBlocksOptions.first,
last = transactionBlocksOptions.last,
before = transactionBlocksOptions.before,
after = transactionBlocksOptions.after,
showBalanceChanges = transactionBlocksOptions.showBalanceChanges,
showEffects = transactionBlocksOptions.showEffects,
showRawEffects = transactionBlocksOptions.showRawEffects,
showEvents = transactionBlocksOptions.showEvents,
showInput = transactionBlocksOptions.showInput,
showObjectChanges = transactionBlocksOptions.showObjectChanges,
showRawInput = transactionBlocksOptions.showRawInput,
filter = transactionBlocksOptions.filter,
first = transactionBlockResponseOptions.first,
last = transactionBlockResponseOptions.last,
before = transactionBlockResponseOptions.before,
after = transactionBlockResponseOptions.after,
showBalanceChanges = transactionBlockResponseOptions.showBalanceChanges,
showEffects = transactionBlockResponseOptions.showEffects,
showRawEffects = transactionBlockResponseOptions.showRawEffects,
showEvents = transactionBlockResponseOptions.showEvents,
showInput = transactionBlockResponseOptions.showInput,
showObjectChanges = transactionBlockResponseOptions.showObjectChanges,
showRawInput = transactionBlockResponseOptions.showRawInput,
filter = transactionBlockResponseOptions.filter,
)
)
)
Expand All @@ -75,3 +135,75 @@ suspend fun queryTransactionBlocks(

return Option.Some(response.data)
}

internal fun signTransaction(message: ByteArray, signer: Account): ByteArray {
return signer.sign(message)
}

@OptIn(ExperimentalEncodingApi::class)
internal suspend fun signAndSubmitTransaction(
config: SuiConfig,
ptb: ProgrammableTransaction,
signer: Account,
gasBudget: ULong,
): Option.Some<ExecuteTransactionBlock.Result?> {

val gasPrice =
when (val gp = getReferenceGasPrice(config)) {
is Option.Some -> gp.value
is Option.None -> throw SuiException("Failed to get gas price")
}

val paymentObject =
when (val po = getCoins(config, SuiAddress.fromString(signer.address.toString()))) {
is Option.Some -> po.value
is Option.None -> throw SuiException("Failed to get payment object")
}

val sightedCoin = paymentObject?.address?.coins?.nodes?.get(0)

val address = sightedCoin?.address ?: throw SuiException("Failed to get address")

val digest =
ObjectDigest(Digest(sightedCoin.digest ?: throw SuiException("Failed to get digest")))

val txData =
TransactionDataComposer.programmable(
sender = SuiAddress.fromString(signer.address.toString()),
gapPayment =
listOf(
ObjectReference(
Reference(AccountAddress.fromString(address)),
sightedCoin.version.toLong(),
digest,
)
),
pt = ptb,
gasBudget = gasBudget,
gasPrice = gasPrice?.toULong() ?: throw SuiException("Failed to get gas price"),
)

val intentMessage = IntentMessage(Intent.suiTransaction(), txData)

val sig = signer.sign(hash(Hash.BLAKE2B256, Bcs.encodeToByteArray(intentMessage)))

val serializedSignatureBytes = byteArrayOf(signer.scheme.scheme) + sig + signer.publicKey.data

val tx = txData with listOf(Base64.encode(serializedSignatureBytes))

val content = tx.content()

val response =
getGraphqlClient(config)
.execute<ExecuteTransactionBlock.Result>(
ExecuteTransactionBlock(
ExecuteTransactionBlock.Variables(txBytes = content.first, signatures = content.second)
)
)

if (response.errors != null) {
throw SuiException(response.errors.toString())
}

return Option.Some(response.data)
}
2 changes: 1 addition & 1 deletion lib/src/commonMain/kotlin/xyz/mcxross/ksui/model/Object.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package xyz.mcxross.ksui.model
import kotlinx.serialization.Serializable
import xyz.mcxross.ksui.util.decodeBase58

@Serializable data class ObjectId(val hash: String)
@Serializable data class ObjectId(val hash: AccountAddress)

@Serializable
data class ObjectReference(val reference: Reference, val version: Long, val digest: ObjectDigest)
Expand Down
Loading

0 comments on commit d09b090

Please sign in to comment.