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

make Transaction and Message immutable #156

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ junitPioneer = "2.3.0"
kermit = "2.0.4"
khash = "1.1.3"
kotlinxCoroutines = "1.9.0"
kotlinLogging = "7.0.0"
ktor = "3.0.1"
okhttp = "4.12.0"
okio = "3.9.1"
Expand All @@ -34,6 +35,7 @@ coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", ver
coroutinesJdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "kotlinxCoroutines" }
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
kotlinLogging = { module = "io.github.oshai:kotlin-logging", version.ref = "kotlinLogging" }
ktorClientContentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktorClientCio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktorClientCore = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
Expand Down
1 change: 1 addition & 0 deletions solana-kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ kotlin {
implementation(libs.kermit)
implementation(libs.okio)
implementation(libs.skie.configurationAnnotations)
implementation(libs.kotlinLogging)
}
}
val commonTest by getting {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,30 @@
package net.avianlabs.solana.domain.core

public class AccountKeysList {
private val accounts: LinkedHashMap<String, AccountMeta> = LinkedHashMap()

public fun add(accountMeta: AccountMeta) {
val key = accountMeta.publicKey.toString()
val existing = accounts[key]
if (existing != null) {
accounts[key] = existing.copy(
isSigner = accountMeta.isSigner || existing.isSigner,
isWritable = accountMeta.isWritable || existing.isWritable,
internal fun List<AccountMeta>.normalize(): List<AccountMeta> = groupBy { it.publicKey }
.mapValues { (_, metas) ->
metas.reduce { acc, meta ->
AccountMeta(
publicKey = acc.publicKey,
isSigner = acc.isSigner || meta.isSigner,
isWritable = acc.isWritable || meta.isWritable,
)
} else {
accounts[key] = accountMeta
}
}

public fun addAll(metas: Collection<AccountMeta>) {
for (meta in metas) {
add(meta)
}
}
.values
.sortedWith(metaComparator)
.toList()

public val list: ArrayList<AccountMeta>
get() {
val accountKeysList = ArrayList(accounts.values)
accountKeysList.sortWith(metaComparator)
return accountKeysList
}

public companion object {
private val metaComparator = Comparator<AccountMeta> { am1, am2 ->
// first sort by signer, then writable
if (am1.isSigner && !am2.isSigner) {
-1
} else if (!am1.isSigner && am2.isSigner) {
1
} else if (am1.isWritable && !am2.isWritable) {
-1
} else if (!am1.isWritable && am2.isWritable) {
1
} else {
0
}
}
private val metaComparator = Comparator<AccountMeta> { am1, am2 ->
// first sort by signer, then writable
if (am1.isSigner && !am2.isSigner) {
-1
} else if (!am1.isSigner && am2.isSigner) {
1
} else if (am1.isWritable && !am2.isWritable) {
-1
} else if (!am1.isWritable && am2.isWritable) {
1
} else {
0
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,173 +1,52 @@
package net.avianlabs.solana.domain.core

import net.avianlabs.solana.tweetnacl.TweetNaCl
import net.avianlabs.solana.tweetnacl.ed25519.PublicKey
import net.avianlabs.solana.vendor.ShortvecEncoding
import net.avianlabs.solana.tweetnacl.vendor.decodeBase58
import okio.Buffer

public class Message(
public var feePayer: PublicKey? = null,
public var recentBlockHash: String? = null,
accountKeys: AccountKeysList = AccountKeysList(),
instructions: List<TransactionInstruction> = emptyList(),
public class Message internal constructor(
wiyarmir marked this conversation as resolved.
Show resolved Hide resolved
public val feePayer: PublicKey?,
public val recentBlockHash: String?,
public val accountKeys: List<AccountMeta>,
public val instructions: List<TransactionInstruction>,
) {

private val _accountKeys: AccountKeysList = accountKeys
private val _instructions: MutableList<TransactionInstruction> = instructions.toMutableList()


public val accountKeys: List<AccountMeta>
get() = _accountKeys.list

public val instructions: List<TransactionInstruction>
get() = _instructions

private class MessageHeader {
var numRequiredSignatures: Byte = 0
var numReadonlySignedAccounts: Byte = 0
var numReadonlyUnsignedAccounts: Byte = 0
fun toByteArray(): ByteArray {
return byteArrayOf(
numRequiredSignatures,
numReadonlySignedAccounts,
numReadonlyUnsignedAccounts
)
}
override fun toString(): String =
"Message(feePayer=$feePayer, recentBlockHash=$recentBlockHash, accountKeys=$accountKeys, instructions=$instructions)"

override fun toString(): String {
return "numRequiredSignatures: $numRequiredSignatures, numReadOnlySignedAccounts: $numReadonlySignedAccounts, numReadOnlyUnsignedAccounts: $numReadonlyUnsignedAccounts"
}
public fun newBuilder(): Builder = Builder(
feePayer = feePayer,
recentBlockHash = recentBlockHash,
accountKeys = accountKeys.toMutableList(),
instructions = instructions.toMutableList(),
)

companion object {
const val HEADER_LENGTH = 3
public class Builder internal constructor(
private var feePayer: PublicKey?,
private var recentBlockHash: String?,
private var accountKeys: MutableList<AccountMeta>,
private var instructions: MutableList<TransactionInstruction>,
) {
public constructor() : this(null, null, mutableListOf(), mutableListOf())

fun fromByteArray(bytes: ByteArray): MessageHeader {
val header = MessageHeader()
header.numRequiredSignatures = bytes[0]
header.numReadonlySignedAccounts = bytes[1]
header.numReadonlyUnsignedAccounts = bytes[2]
return header
}
public fun setFeePayer(feePayer: PublicKey): Builder {
this.feePayer = feePayer
return this
}
}

private class CompiledInstruction {
var programIdIndex: Byte = 0
lateinit var keyIndicesCount: ByteArray
lateinit var keyIndices: ByteArray
lateinit var dataLength: ByteArray
lateinit var data: ByteArray

// 1 = programIdIndex length
val length: Int
get() =// 1 = programIdIndex length
1 + keyIndicesCount.size + keyIndices.size + dataLength.size + data.size
}

public fun addInstruction(instruction: TransactionInstruction): Message {
_accountKeys.addAll(instruction.keys)
_accountKeys.add(AccountMeta(instruction.programId, false, false))
_instructions.add(instruction)
return this
}

public fun serialize(): ByteArray {
requireNotNull(recentBlockHash) { "recentBlockhash required" }
require(_instructions.size != 0) { "No instructions provided" }
val messageHeader = MessageHeader()
val keysList = compileAccountKeys()
val accountKeysSize = keysList.size
val accountAddressesLength = ShortvecEncoding.encodeLength(accountKeysSize)
var compiledInstructionsLength = 0
val compiledInstructions: MutableList<CompiledInstruction> = ArrayList()
for (instruction in _instructions) {
val keysSize = instruction.keys.size
val keyIndices = ByteArray(keysSize)
for (i in 0 until keysSize) {
keyIndices[i] = findAccountIndex(keysList, instruction.keys[i].publicKey).toByte()
}
val compiledInstruction = CompiledInstruction()
compiledInstruction.programIdIndex =
findAccountIndex(keysList, instruction.programId).toByte()
compiledInstruction.keyIndicesCount = ShortvecEncoding.encodeLength(keysSize)
compiledInstruction.keyIndices = keyIndices
compiledInstruction.dataLength = ShortvecEncoding.encodeLength(instruction.data.count())
compiledInstruction.data = instruction.data
compiledInstructions.add(compiledInstruction)
compiledInstructionsLength += compiledInstruction.length
}
val instructionsLength = ShortvecEncoding.encodeLength(compiledInstructions.size)
val accountsKeyBufferSize = accountKeysSize * TweetNaCl.Signature.PUBLIC_KEY_BYTES
val bufferSize =
(MessageHeader.HEADER_LENGTH + RECENT_BLOCK_HASH_LENGTH + accountAddressesLength.size
+ accountsKeyBufferSize + instructionsLength.size
+ compiledInstructionsLength)
val out = Buffer()
val accountKeysBuff = Buffer()
for (accountMeta in keysList) {
accountKeysBuff.write(accountMeta.publicKey.toByteArray())
if (accountMeta.isSigner) {
messageHeader.numRequiredSignatures =
(messageHeader.numRequiredSignatures.plus(1)).toByte()
if (!accountMeta.isWritable) {
messageHeader.numReadonlySignedAccounts =
(messageHeader.numReadonlySignedAccounts.plus(1)).toByte()
}
} else {
if (!accountMeta.isWritable) {
messageHeader.numReadonlyUnsignedAccounts =
(messageHeader.numReadonlyUnsignedAccounts.plus(1)).toByte()
}
}
public fun setRecentBlockHash(recentBlockHash: String): Builder {
this.recentBlockHash = recentBlockHash
return this
}
out.write(messageHeader.toByteArray())
out.write(accountAddressesLength)
out.write(accountKeysBuff, accountsKeyBufferSize.toLong())
out.write(recentBlockHash!!.decodeBase58())
out.write(instructionsLength)
for (compiledInstruction in compiledInstructions) {
out.writeByte(compiledInstruction.programIdIndex.toInt())
out.write(compiledInstruction.keyIndicesCount)
out.write(compiledInstruction.keyIndices)
out.write(compiledInstruction.dataLength)
out.write(compiledInstruction.data)
}
return out.readByteArray(bufferSize.toLong())
}

private fun compileAccountKeys(): List<AccountMeta> {
val keysList: MutableList<AccountMeta> = _accountKeys.list
val newList: MutableList<AccountMeta> = ArrayList()
try {
val feePayerIndex = findAccountIndex(keysList, feePayer!!)
val feePayerMeta = keysList[feePayerIndex]
newList.add(AccountMeta(feePayerMeta.publicKey, true, true))
keysList.removeAt(feePayerIndex)
} catch (e: RuntimeException) { // Fee payer not yet in list
newList.add(AccountMeta(feePayer!!, true, true))
public fun addInstruction(instruction: TransactionInstruction): Builder {
accountKeys.addAll(
instruction.keys +
AccountMeta(instruction.programId, isSigner = false, isWritable = false)
)
instructions += instruction
return this
}
newList.addAll(keysList)
return newList
}

private fun findAccountIndex(accountMetaList: List<AccountMeta>, key: PublicKey): Int {
for (i in accountMetaList.indices) {
if (accountMetaList[i].publicKey.equals(key)) {
return i
}
}
throw RuntimeException("unable to find account index")
public fun build(): Message =
Message(feePayer, recentBlockHash, accountKeys.normalize(), instructions)
}

override fun toString(): String =
"""Message(
| header: not set,
| accountKeys: [${_accountKeys.list.joinToString()}],
| recentBlockhash: $recentBlockHash,
| instructions: [${_instructions.joinToString()}]
|)""".trimMargin()

}

private const val RECENT_BLOCK_HASH_LENGTH = 32
Loading
Loading