-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #156 from avianlabs/guillermo/make-transaction-and…
…-signedtransaction-immutable make Transaction and Message immutable
- Loading branch information
Showing
20 changed files
with
452 additions
and
349 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 22 additions & 40 deletions
62
solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/core/AccountKeysList.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
36 changes: 0 additions & 36 deletions
36
solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/core/CompiledInstruction.kt
This file was deleted.
Oops, something went wrong.
191 changes: 34 additions & 157 deletions
191
solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/core/Message.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.