Skip to content

Commit

Permalink
Merge pull request #156 from avianlabs/guillermo/make-transaction-and…
Browse files Browse the repository at this point in the history
…-signedtransaction-immutable

make Transaction and Message immutable
  • Loading branch information
wiyarmir authored Nov 12, 2024
2 parents 25b5b5a + 09eb2d6 commit 396356f
Show file tree
Hide file tree
Showing 20 changed files with 452 additions and 349 deletions.
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,50 @@
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(),
@ConsistentCopyVisibility
public data class Message private constructor(
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 fun newBuilder(): Builder = Builder(
feePayer = feePayer,
recentBlockHash = recentBlockHash,
accountKeys = accountKeys.toMutableList(),
instructions = instructions.toMutableList(),
)

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())

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
)
public fun setFeePayer(feePayer: PublicKey): Builder {
this.feePayer = feePayer
return this
}

override fun toString(): String {
return "numRequiredSignatures: $numRequiredSignatures, numReadOnlySignedAccounts: $numReadonlySignedAccounts, numReadOnlyUnsignedAccounts: $numReadonlyUnsignedAccounts"
}

companion object {
const val HEADER_LENGTH = 3

fun fromByteArray(bytes: ByteArray): MessageHeader {
val header = MessageHeader()
header.numRequiredSignatures = bytes[0]
header.numReadonlySignedAccounts = bytes[1]
header.numReadonlyUnsignedAccounts = bytes[2]
return header
}
public fun setRecentBlockHash(recentBlockHash: String): Builder {
this.recentBlockHash = recentBlockHash
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()
}
}
}
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)
public fun addInstruction(instruction: TransactionInstruction): Builder {
accountKeys.addAll(
instruction.keys +
AccountMeta(instruction.programId, isSigner = false, isWritable = false)
)
instructions += instruction
return this
}
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))
}
newList.addAll(keysList)
return newList
public fun build(): Message =
Message(feePayer, recentBlockHash, accountKeys.normalize(), instructions)
}

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")
}

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

0 comments on commit 396356f

Please sign in to comment.