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

Feature/token 2022 extension #88

Merged
merged 7 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
@@ -0,0 +1,69 @@
import Foundation

public struct AnyToken2022ExtensionState: BorshCodable, Codable, Equatable, Hashable {
// MARK: - Hashable

public func hash(into hasher: inout Hasher) {
type.hash(into: &hasher)
state.hash(into: &hasher)
}

// MARK: - Equatable

public static func == (lhs: AnyToken2022ExtensionState, rhs: AnyToken2022ExtensionState) -> Bool {
lhs.type == rhs.type &&
lhs.state.jsonString == rhs.state.jsonString
}

// MARK: - Properties

public let type: Token2022ExtensionType
public let state: any Token2022ExtensionState

// MARK: - Codable

enum CodingKeys: String, CodingKey {
case type
case state
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
type = try container.decode(Token2022ExtensionType.self, forKey: .type)

switch type {
case .transferFeeConfig:
state = try container.decode(TransferFeeConfigExtensionState.self, forKey: .state)
default:
state = try container.decode(UnparsedExtensionState.self, forKey: .state)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
try container.encode(state, forKey: .state)
}

// MARK: - BorshCodable

public init(from reader: inout BinaryReader) throws {
guard let type = try Token2022ExtensionType(rawValue: UInt16(from: &reader)) else {
throw BinaryReaderError.dataMismatch
}
self.type = type
switch type {
case .transferFeeConfig:
state = try TransferFeeConfigExtensionState(from: &reader)
case .interestBearingConfig:
state = try InterestBearingConfigExtensionState(from: &reader)
default:
state = try UnparsedExtensionState(from: &reader)
}
}

public func serialize(to data: inout Data) throws {
try type.rawValue.serialize(to: &data)
try state.serialize(to: &data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

public struct InterestBearingConfigExtensionState: Token2022ExtensionState {
public var length: UInt16

public let rateAuthority: PublicKey
public let initializationTimestamp: Int64
public let preUpdateAverageRate: Int16
public let lastUpdateTimestamp: Int64
public let currentRate: Int16

public init(from reader: inout BinaryReader) throws {
length = try UInt16(from: &reader)
rateAuthority = try PublicKey(from: &reader)
initializationTimestamp = try Int64(from: &reader)
preUpdateAverageRate = try Int16(from: &reader)
lastUpdateTimestamp = try Int64(from: &reader)
currentRate = try Int16(from: &reader)
}

public func serialize(to data: inout Data) throws {
try length.serialize(to: &data)
try rateAuthority.serialize(to: &data)
try initializationTimestamp.serialize(to: &data)
try preUpdateAverageRate.serialize(to: &data)
try lastUpdateTimestamp.serialize(to: &data)
try currentRate.serialize(to: &data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

public struct TransferFeeConfigExtensionState: Token2022ExtensionState {
public struct TransferFee: BorshCodable, Codable, Equatable, Hashable {
public let epoch: UInt64
public let maximumFee: UInt64
public let transferFeeBasisPoints: UInt16

public init(from reader: inout BinaryReader) throws {
epoch = try UInt64(from: &reader)
maximumFee = try UInt64(from: &reader)
transferFeeBasisPoints = try UInt16(from: &reader)
}

public func serialize(to data: inout Data) throws {
try epoch.serialize(to: &data)
try maximumFee.serialize(to: &data)
try transferFeeBasisPoints.serialize(to: &data)
}
}

public let length: UInt16
/// Optional authority to set the fee
public let transferFeeConfigAuthority: PublicKey
/// Withdraw from mint instructions must be signed by this key
public let withdrawWithHeldAuthority: PublicKey
/// Withheld transfer fee tokens that have been moved to the mint for
/// withdrawal
public let withheldAmount: UInt64
/// Older transfer fee, used if the current epoch < new_transfer_fee.epoch
public let olderTransferFee: TransferFee
/// Newer transfer fee, used if the current epoch >= new_transfer_fee.epoch
public let newerTransferFee: TransferFee

public init(from reader: inout BinaryReader) throws {
length = try UInt16(from: &reader)
transferFeeConfigAuthority = try PublicKey(from: &reader)
withdrawWithHeldAuthority = try PublicKey(from: &reader)
withheldAmount = try UInt64(from: &reader)
olderTransferFee = try TransferFee(from: &reader)
newerTransferFee = try TransferFee(from: &reader)
}

public func serialize(to data: inout Data) throws {
try length.serialize(to: &data)
try transferFeeConfigAuthority.serialize(to: &data)
try withdrawWithHeldAuthority.serialize(to: &data)
try withheldAmount.serialize(to: &data)
try olderTransferFee.serialize(to: &data)
try newerTransferFee.serialize(to: &data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

struct UnparsedExtensionState: Token2022ExtensionState {
let length: UInt16
let data: Data
init(from reader: inout BinaryReader) throws {
length = try UInt16(from: &reader)
data = try Data(reader.read(count: Int(length)))
}

func serialize(to data: inout Data) throws {
try length.serialize(to: &data)
try data.serialize(to: &data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public protocol Token2022ExtensionState: BorshCodable, Codable, Equatable, Hashable {
var length: UInt16 { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Foundation

public enum Token2022ExtensionType: UInt16, Codable, Hashable {
/// Used as padding if the account size would otherwise be 355, same as a
/// multisig
case uninitialized
/// Includes transfer fee rate info and accompanying authorities to withdraw
/// and set the fee
case transferFeeConfig
/// Includes withheld transfer fees
case transferFeeAmount
/// Includes an optional mint close authority
case mintCloseAuthority
/// Auditor configuration for confidential transfers
case confidentialTransferMint
/// State for confidential transfers
case confidentialTransferAccount
/// Specifies the default Account::state for new Accounts
case defaultAccountState
/// Indicates that the Account owner authority cannot be changed
case immutableOwner
/// Require inbound transfers to have memo
case memoTransfer
/// Indicates that the tokens from this mint can't be transferred
case nonTransferable
/// Tokens accrue interest over time,
case interestBearingConfig
/// Locks privileged token operations from happening via CPI
case cpiGuard
/// Includes an optional permanent delegate
case permanentDelegate
/// Indicates that the tokens in this account belong to a non-transferable
/// mint
case nonTransferableAccount
/// Mint requires a CPI to a program implementing the "transfer hook"
/// interface
case transferHook
/// Indicates that the tokens in this account belong to a mint with a
/// transfer hook
case transferHookAccount
/// Includes encrypted withheld fees and the encryption public that they are
/// encrypted under
case confidentialTransferFeeConfig
/// Includes confidential withheld transfer fees
case confidentialTransferFeeAmount
/// Mint contains a pointer to another account (or the same account) that
/// holds metadata
case metadataPointer
/// Mint contains token-metadata
case tokenMetadata
/// Mint contains a pointer to another account (or the same account) that
/// holds group configurations
case groupPointer
/// Mint contains token group configurations
case tokenGroup
/// Mint contains a pointer to another account (or the same account) that
/// holds group member configurations
case groupMemberPointer
/// Mint contains token group member configurations
case tokenGroupMember

// MARK: - Test only

// /// Test variable-length mint extension
// case variableLenMintTest = UInt16.max - 2,
// /// Padding extension used to make an account exactly Multisig::LEN, used
// /// for testing
// case accountPaddingTest = UInt16.max - 1
// /// Padding extension used to make a mint exactly Multisig::LEN, used for
// /// testing
// case mintPaddingTest = UInt16.max
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public struct Token2022AccountState: TokenAccountState {
public let closeAuthorityOption: UInt32
public var closeAuthority: PublicKey?

public var extensions: [AnyToken2022ExtensionState]

public init(
mint: PublicKey,
owner: PublicKey,
Expand All @@ -32,7 +34,8 @@ public struct Token2022AccountState: TokenAccountState {
isNative: Bool,
delegatedAmount: UInt64,
closeAuthorityOption: UInt32,
closeAuthority: PublicKey? = nil
closeAuthority: PublicKey? = nil,
extensions: [AnyToken2022ExtensionState] = []
) {
self.mint = mint
self.owner = owner
Expand All @@ -49,13 +52,16 @@ public struct Token2022AccountState: TokenAccountState {
self.delegatedAmount = delegatedAmount
self.closeAuthorityOption = closeAuthorityOption
self.closeAuthority = closeAuthority
self.extensions = extensions
}
}

extension Token2022AccountState: BorshCodable {
public func serialize(to writer: inout Data) throws {
try serializeCommonProperties(to: &writer)
// TODO: - Serialize token-2022 extensions here
for ext in extensions {
try ext.serialize(to: &writer)
}
}

public init(from reader: inout BinaryReader) throws {
Expand All @@ -75,5 +81,20 @@ extension Token2022AccountState: BorshCodable {
delegatedAmount = oldTokenProgramData.delegatedAmount
closeAuthorityOption = oldTokenProgramData.closeAuthorityOption
closeAuthority = oldTokenProgramData.closeAuthority

guard reader.cursor < reader.bytes.count else {
extensions = []
return
}

_ = try reader.read(count: 1) // account type

var extensions = [AnyToken2022ExtensionState]()
repeat {
let ext = try AnyToken2022ExtensionState(from: &reader)
extensions.append(ext)
} while reader.cursor < reader.bytes.count

self.extensions = extensions
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public struct Token2022MintState: TokenMintState {
public let isInitialized: Bool
public let freezeAuthorityOption: UInt32
public let freezeAuthority: PublicKey?

public var extensions: [AnyToken2022ExtensionState]
}

extension Token2022MintState: BorshCodable {
Expand All @@ -20,10 +22,28 @@ extension Token2022MintState: BorshCodable {
isInitialized = oldTokenMintState.isInitialized
freezeAuthorityOption = oldTokenMintState.freezeAuthorityOption
freezeAuthority = oldTokenMintState.freezeAuthority

guard reader.cursor < reader.bytes.count else {
extensions = []
return
}

_ = try reader.read(count: 83) // padding
_ = try reader.read(count: 1) // mint type

var extensions = [AnyToken2022ExtensionState]()
repeat {
let ext = try AnyToken2022ExtensionState(from: &reader)
extensions.append(ext)
} while reader.cursor < reader.bytes.count

self.extensions = extensions
}

public func serialize(to writer: inout Data) throws {
try serializeCommonProperties(to: &writer)
// TODO: - Serialize token-2022 extensions here
for ext in extensions {
try ext.serialize(to: &writer)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ class BufferLayoutTests: XCTestCase {

// MARK: - Account info


func testDecodingAccountInfo() throws {
XCTAssertEqual(SPLTokenAccountState.BUFFER_LENGTH, 165)

Expand Down Expand Up @@ -168,4 +167,39 @@ class BufferLayoutTests: XCTestCase {
var binaryReader = BinaryReader(bytes: data.bytes)
let _ = try EmptyInfo(from: &binaryReader)
}

// MARK: - Token2022

func testDecodingToken2022MintState() throws {
let string =
"AAAAAAT3LznRbp1toHmr0Mjv1bBjc6oSrtihgQu/PG0Sunz6XUTVg3ktAAAFAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT3LznRbp1toHmr0Mjv1bBjc6oSrtihgQu/PG0Sunz6N5bilgAAAAASAgAAAAAAAAAgPYh5LQAALAESAgAAAAAAAAAgPYh5LQAALAE="
let data = Data(base64Encoded: string)!
var binaryReader = BinaryReader(bytes: data.bytes)
let state = try Token2022MintState(from: &binaryReader)

XCTAssertEqual(state.extensions.count, 1)
}

func testDecodingToken2022MintState2() throws {
// Mint FZYEgCWzzedxcmxYvGXSkMrj7TaA3bXoaEv6XMnwtLKh
let string =
"AAAAABdZNqd8UPqRoeBHXdhoEwzZNLf6UnDQ1UDsr4oXimfhquOLA1BVIXECAQAAAAAXWTanfFD6kaHgR13YaBMM2TS3+lJw0NVA7K+KF4pn4QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALoDKJKHBCRLpAQAAAAAAAACQI15ZrVt7LAHpAQAAAAAAAACQI15ZrVt7LAEKADQAF1k2p3xQ+pGh4Edd2GgTDNk0t/pScNDVQOyviheKZ+EN9NlkAAAAAAAADfTZZAAAAAAAAAYAAQAB"
let data = Data(base64Encoded: string)!
var binaryReader = BinaryReader(bytes: data.bytes)
let state = try Token2022MintState(from: &binaryReader)

XCTAssertEqual(state.extensions.count, 3)
XCTAssertTrue(state.extensions[0].state is TransferFeeConfigExtensionState)
XCTAssertTrue(state.extensions[1].state is InterestBearingConfigExtensionState)
}

func testDecodingToken2022AccountState() throws {
let string =
"c8d675Tc8/enuGEbVogbaWoW6iY9JFkJIswLnf/gvCXDAcw04n4gWtOj5P12Rb7RAxY9RRwFQOwFWCWPS3OnJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgcAAAA="
let data = Data(base64Encoded: string)!
var binaryReader = BinaryReader(bytes: data.bytes)
let state = try Token2022AccountState(from: &binaryReader)

XCTAssertEqual(state.extensions.count, 1)
}
}