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

feat: Implement txn execution, simulation, and signing; Get programable txn construction working #17

Merged
merged 1 commit into from
Aug 26, 2024
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
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
Loading