Skip to content

Commit

Permalink
Merge pull request #211 from MirekR/feature/transaction-deserialisation
Browse files Browse the repository at this point in the history
Implement Transaction deserialisation
  • Loading branch information
ajamaica authored Oct 27, 2022
2 parents 5ca99bf + ea6cbd5 commit 78cd082
Show file tree
Hide file tree
Showing 4 changed files with 408 additions and 21 deletions.
134 changes: 123 additions & 11 deletions Sources/Solana/Models/SendingTransaction/Message.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import Foundation

struct CompiledInstruction {
let programIdIndex: Int
let accounts: [Int]
let data: [UInt8]
}

extension Transaction {
struct Message {
static let PUBKEY_LENGTH = 32

// MARK: - Constants
private static let RECENT_BLOCK_HASH_LENGTH = 32

Expand All @@ -22,7 +30,7 @@ extension Transaction {
// + compiledInstructionsLength

var data = Data(/*capacity: bufferSize*/)

// Compiled instruction
return encodeHeader().map { data.append($0) }
.flatMap { _ in return encodeAccountKeys().map { data.append($0) } }
Expand Down Expand Up @@ -57,11 +65,6 @@ extension Transaction {
return encodeLength(accountKeys.count).map { keyCount in
// construct data
var data = Data(capacity: keyCount.count + accountKeys.count * PublicKey.LENGTH)
// sort
let signedKeys = accountKeys.filter {$0.isSigner}
let unsignedKeys = accountKeys.filter {!$0.isSigner}
let accountKeys = signedKeys + unsignedKeys

// append data
data.append(keyCount)
for meta in accountKeys {
Expand All @@ -76,7 +79,7 @@ extension Transaction {
}

private func encodeInstructions() -> Result<Data, Error> {
var compiledInstructions = [CompiledInstruction]()
var compiledInstructions = [SerialiseCompiledInstruction]()

for instruction in programInstructions {

Expand All @@ -93,7 +96,7 @@ extension Transaction {
}

do {
let compiledInstruction = CompiledInstruction(
let compiledInstruction = SerialiseCompiledInstruction(
programIdIndex: UInt8(try accountKeys.index(ofElementWithPublicKey: instruction.programId).get()),
keyIndicesCount: [UInt8](Data.encodeLength(keysSize)),
keyIndices: [UInt8](keyIndices),
Expand All @@ -120,18 +123,25 @@ extension Transaction {
extension Transaction.Message {
// MARK: - Nested type
public struct Header: Decodable {
static let LENGTH = 3
// TODO:
var numRequiredSignatures: UInt8 = 0
var numReadonlySignedAccounts: UInt8 = 0
var numReadonlyUnsignedAccounts: UInt8 = 0


init() {}

init(numRequiredSignatures: UInt8, numReadonlySignedAccounts: UInt8, numReadonlyUnsignedAccounts: UInt8) {
self.numRequiredSignatures = numRequiredSignatures
self.numReadonlySignedAccounts = numReadonlySignedAccounts
self.numReadonlyUnsignedAccounts = numReadonlyUnsignedAccounts
}

var bytes: [UInt8] {
[numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts]
}
}

struct CompiledInstruction {
struct SerialiseCompiledInstruction {
let programIdIndex: UInt8
let keyIndicesCount: [UInt8]
let keyIndices: [UInt8]
Expand All @@ -146,4 +156,106 @@ extension Transaction.Message {
Data([programIdIndex] + keyIndicesCount + keyIndices + dataLength + data)
}
}

static func from(buffer: Data) throws -> Transaction.Message {
// Slice up wire data
var byteArray = buffer

guard let numRequiredSignatures = byteArray.first else { throw SolanaError.invalidRequest(reason: "Could not parse number of required signatures") }
byteArray = Data(byteArray.dropFirst())

guard let numReadonlySignedAccounts = byteArray.first else { throw SolanaError.invalidRequest(reason: "Could not parse number of signed accounts") }
byteArray = Data(byteArray.dropFirst())

guard let numReadonlyUnsignedAccounts = byteArray.first else { throw SolanaError.invalidRequest(reason: "Could not parse number of unsigned accounts") }
byteArray = Data(byteArray.dropFirst())

let accountsBlock = Shortvec.nextBlock(buffer: byteArray, multiplier: PUBKEY_LENGTH)
byteArray = accountsBlock.1

let accountKeys = accountsBlock.0
.bytes
.chunked(into: PUBKEY_LENGTH)
.map { Base58.encode($0) }

let recentBlockhash = byteArray[0..<PUBKEY_LENGTH].bytes
byteArray = Data(byteArray.dropFirst(PUBKEY_LENGTH))

let instructionsBlock = Shortvec.decodeLength(buffer: byteArray)
byteArray = instructionsBlock.1

var instructions: [CompiledInstruction] = []
for _ in 0...(instructionsBlock.0 - 1) {
guard let programIdIndex = byteArray.firstAsInt() else { break }

byteArray = Data(byteArray.dropFirst())

let accountBlock = Shortvec.nextBlock(buffer: byteArray)
byteArray = accountBlock.1

let accounts = accountBlock.0.bytes.map{ Int($0) }

let dataBlock = Shortvec.nextBlock(buffer: byteArray)
byteArray = dataBlock.1

let compiledInstruction = CompiledInstruction(programIdIndex: programIdIndex, accounts: accounts, data: dataBlock.0.bytes)
instructions.append(compiledInstruction)
}

let header = Header(
numRequiredSignatures: numRequiredSignatures,
numReadonlySignedAccounts: numReadonlySignedAccounts,
numReadonlyUnsignedAccounts: numReadonlyUnsignedAccounts
)

let accountMetas = keysToAccountMetas(accountKeys: accountKeys, header: header)
let accountMetasAsDictionary = Dictionary(uniqueKeysWithValues: accountMetas.map{ ($0.publicKey.base58EncodedString, $0) })

var programInstructions: [TransactionInstruction] = []
for instruction in instructions {
// TODO: Not sure if it should continue or throw, but I don't think it can happen here
guard let programId = PublicKey(string: accountKeys[instruction.programIdIndex]) else { continue }

var keys: [AccountMeta] = []
for j in 0...(instruction.accounts.count - 1) {
let accountIndex = instruction.accounts[j]
let pubKey = accountKeys[accountIndex]
// TODO: Not sure if it should continue or throw, but I don't think it can happen here
guard let accountMeta = accountMetasAsDictionary[pubKey] else { continue }
keys.append(accountMeta)
}

let transactionInstruction = TransactionInstruction(keys: keys, programId: programId, data: instruction.data)
programInstructions.append(transactionInstruction)
}

return Transaction.Message(accountKeys: accountMetas, recentBlockhash: Base58.encode(recentBlockhash), programInstructions: programInstructions)
}

static func keysToAccountMetas(accountKeys: [String], header: Header) -> [AccountMeta] {
var accountMetas: [AccountMeta] = []

for (index, key) in accountKeys.enumerated() {
guard let account = PublicKey(string: key) else { continue }
let isSigner = header.isAccountSigner(index: index)
let isWritable = header.isAccountWritable(index: index, accountKeysCount: accountKeys.count)

let accountMeta = AccountMeta(publicKey: account, isSigner: isSigner, isWritable: isWritable)
accountMetas.append(accountMeta)
}

return accountMetas
}
}

extension Transaction.Message.Header {
func isAccountSigner(index: Int) -> Bool {
return index < self.numRequiredSignatures
}

func isAccountWritable(index: Int, accountKeysCount: Int) -> Bool {
return index < self.numRequiredSignatures - self.numReadonlySignedAccounts ||
(index >= self.numRequiredSignatures &&
index < accountKeysCount - Int(self.numReadonlyUnsignedAccounts))
}
}
101 changes: 91 additions & 10 deletions Sources/Solana/Models/SendingTransaction/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import Foundation
import TweetNacl

public struct Transaction {
private var signatures = [Signature]()
private static let SIGNATURE_LENGTH: Int = 64
private static let DEFAULT_SIGNATURE = Data(capacity: 0)

var signatures = [Signature]()
private let feePayer: PublicKey
private let recentBlockhash: String

Expand Down Expand Up @@ -136,12 +139,21 @@ public struct Transaction {
return message
}
}
signatures = signedKeys.map {Signature(signature: nil, publicKey: $0.publicKey)}

return message
}
}

private func compileMessage() -> Result<Message, Error> {
static func sortAccountMetas(accountMetas: [AccountMeta]) -> [AccountMeta] {
let locale = Locale(identifier: "en_US")
return accountMetas.sorted { (x, y) -> Bool in
if x.isSigner != y.isSigner {return x.isSigner}
if x.isWritable != y.isWritable {return x.isWritable}
return x.publicKey.base58EncodedString.compare(y.publicKey.base58EncodedString, locale: locale) == .orderedAscending
}
}

func compileMessage() -> Result<Message, Error> {
// verify instructions
guard instructions.count > 0 else {
return .failure(SolanaError.other("No instructions provided"))
Expand All @@ -165,11 +177,7 @@ public struct Transaction {
}

// sort accountMetas, first by signer, then by writable
accountMetas.sort { (x, y) -> Bool in
if x.isSigner != y.isSigner {return x.isSigner}
if x.isWritable != y.isWritable {return x.isWritable}
return false
}
accountMetas = Transaction.sortAccountMetas(accountMetas: accountMetas)

// filterOut duplicate account metas, keeps writable one
accountMetas = accountMetas.reduce([AccountMeta](), {result, accountMeta in
Expand Down Expand Up @@ -230,8 +238,6 @@ public struct Transaction {
}
}

accountMetas = signedKeys + unsignedKeys

return .success(Message(
accountKeys: accountMetas,
recentBlockhash: recentBlockhash,
Expand Down Expand Up @@ -286,6 +292,7 @@ public struct Transaction {
}

public extension Transaction {

struct Signature {
var signature: Data?
var publicKey: PublicKey
Expand All @@ -295,4 +302,78 @@ public extension Transaction {
self.publicKey = publicKey
}
}

static func from(buffer: Data) throws -> Transaction {
// Slice up wire data
var byteArray = buffer

let signatureCount = Shortvec.decodeLength(buffer: byteArray)
byteArray = signatureCount.1

var signatures: [[UInt8]] = []
for _ in 0...(signatureCount.0) - 1 {
let signature = byteArray[0..<SIGNATURE_LENGTH]
byteArray = Data(byteArray.dropFirst(SIGNATURE_LENGTH))

signatures.append(signature.bytes)
}

return try populateTransaction(fromMessage: Message.from(buffer: byteArray), signatures: signatures)
}

private static func populateTransaction(fromMessage: Message, signatures: [[UInt8]]) throws -> Transaction {

// TODO: Should check against required number of signatures if there are any
let feePayer = fromMessage.accountKeys[0].publicKey

var sigs: [Transaction.Signature] = []

for (index, signature) in signatures.enumerated() {
let signatureEncoded = Base58.encode(signature) == Base58.encode(DEFAULT_SIGNATURE.bytes) ? nil : signature

let publicKey = fromMessage.accountKeys[index].publicKey

sigs.append(Transaction.Signature(signature: signatureEncoded.map { Data($0) }, publicKey: publicKey))
}

return Transaction(signatures: sigs, feePayer: feePayer, instructions: fromMessage.programInstructions, recentBlockhash: fromMessage.recentBlockhash)
}
}

public class Shortvec {
static func decodeLength(buffer: Data) -> (Int, Data) {
var newBytes = buffer
var len = 0
var size = 0
while (true) {
guard let elem = newBytes.firstAsInt() else {
break
}

newBytes = Data(newBytes.dropFirst(1))

len = len | (elem & 0x7f) << (size * 7)
size += 1

if ((elem & 0x80) == 0) {
break
}
}

return (len, newBytes)
}

static func nextBlock(buffer: Data, multiplier: Int = 1) -> (Data, Data) {
let nextLengh = decodeLength(buffer: buffer)

let block = Data(nextLengh.1[0..<(nextLengh.0 * multiplier)])

return (block, Data(nextLengh.1.dropFirst(nextLengh.0 * multiplier)))
}
}

public extension Data {
func firstAsInt() -> Int? {
return self.first.map { Int($0) }
}
}
2 changes: 2 additions & 0 deletions Tests/SolanaTests/AsyncAssertThrowing.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Foundation
import XCTest

@available(iOS 13.0.0, *)
@available(macOS 10.15, *)
func asyncAssertThrowing<Out>(_ message: String, file: StaticString = #file, line: UInt = #line, block: () async throws -> Out) async {
do {
_ = try await block()
Expand Down
Loading

0 comments on commit 78cd082

Please sign in to comment.