diff --git a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift index 874bd04..7aee51c 100644 --- a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift @@ -968,7 +968,7 @@ public class TzKTClient { // Main transactions var urlMain = config.tzktURL urlMain.appendPathComponent("v1/accounts/\(address)/operations") - urlMain.appendQueryItem(name: "type", value: "delegation,origination,transaction") + urlMain.appendQueryItem(name: "type", value: "delegation,origination,transaction,staking") urlMain.appendQueryItem(name: "micheline", value: 1) urlMain.appendQueryItem(name: "limit", value: limit) diff --git a/Sources/KukaiCoreSwift/Models/BakingBad/TzKTTransaction.swift b/Sources/KukaiCoreSwift/Models/BakingBad/TzKTTransaction.swift index 50576e1..1de4768 100644 --- a/Sources/KukaiCoreSwift/Models/BakingBad/TzKTTransaction.swift +++ b/Sources/KukaiCoreSwift/Models/BakingBad/TzKTTransaction.swift @@ -30,6 +30,7 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident case delegation case origination case transaction + case staking case reveal case batch case unknown @@ -42,6 +43,9 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident case reveal case exchange case contractCall + case stake + case unstake + case finaliseUnstake case batch case unknown } @@ -74,6 +78,7 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident public let hasInternals: Bool public let tokenTransfersCount: Decimal? public let errors: [TransactionError]? + public let kind: String? public let date: Date? public var tzktTokenTransfer: TzKTTokenTransfer? = nil { @@ -110,11 +115,11 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident // MARK: - Codable Protocol public enum CodingKeys: String, CodingKey { - case type, id, level, timestamp, hash, counter, initiater, sender, bakerFee, storageFee, allocationFee, target, prevDelegate, newDelegate, amount, parameter, status, subType, entrypointCalled, primaryToken, hasInternals, tokenTransfersCount, errors + case type, id, level, timestamp, hash, counter, initiater, sender, bakerFee, storageFee, allocationFee, target, prevDelegate, newDelegate, amount, parameter, status, subType, entrypointCalled, primaryToken, hasInternals, tokenTransfersCount, errors, kind } /// Manually init a `TzKTTransaction` - public init(type: TransactionType, id: Decimal, level: Decimal, timestamp: String, hash: String, counter: Decimal, initiater: TzKTAddress?, sender: TzKTAddress, bakerFee: XTZAmount, storageFee: XTZAmount, allocationFee: XTZAmount, target: TzKTAddress?, prevDelegate: TzKTAddress?, newDelegate: TzKTAddress?, amount: TokenAmount, parameter: [String: String]?, status: TransactionStatus, hasInternals: Bool, tokenTransfersCount: Decimal?, errors: [TransactionError]?) { + public init(type: TransactionType, id: Decimal, level: Decimal, timestamp: String, hash: String, counter: Decimal, initiater: TzKTAddress?, sender: TzKTAddress, bakerFee: XTZAmount, storageFee: XTZAmount, allocationFee: XTZAmount, target: TzKTAddress?, prevDelegate: TzKTAddress?, newDelegate: TzKTAddress?, amount: TokenAmount, parameter: [String: String]?, status: TransactionStatus, hasInternals: Bool, tokenTransfersCount: Decimal?, errors: [TransactionError]?, kind: String?) { self.type = type self.id = id @@ -138,6 +143,7 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident self.date = TzKTTransaction.dateFormatter.date(from: timestamp) self.errors = errors + self.kind = kind } /// Convert a `TzKTTokenTransfer` into a `TzKTTransaction` @@ -168,6 +174,7 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident self.tzktTokenTransfer = from self.errors = nil + self.kind = nil } public init(from decoder: Decoder) throws { @@ -221,6 +228,7 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident } self.errors = try? container.decodeIfPresent([TransactionError].self, forKey: .errors) + self.kind = try? container.decodeIfPresent(String.self, forKey: .kind) } public func encode(to encoder: Encoder) throws { @@ -249,14 +257,15 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident try container.encodeIfPresent(subType, forKey: .subType) try container.encodeIfPresent(entrypointCalled, forKey: .entrypointCalled) try container.encodeIfPresent(primaryToken, forKey: .primaryToken) + try container.encodeIfPresent(kind, forKey: .kind) } /// Used for creating "Pending" transactions - public static func placeholder(withStatus status: TransactionStatus, id: Decimal, opHash: String, type: TransactionType, counter: Decimal, fromWallet: WalletMetadata, destination: TzKTAddress, xtzAmount: TokenAmount, parameters: [String: String]?, primaryToken: Token?) -> TzKTTransaction { + public static func placeholder(withStatus status: TransactionStatus, id: Decimal, opHash: String, type: TransactionType, counter: Decimal, fromWallet: WalletMetadata, destination: TzKTAddress, xtzAmount: TokenAmount, parameters: [String: String]?, primaryToken: Token?, kind: String?) -> TzKTTransaction { let timestamp = TzKTTransaction.dateFormatter.string(from: Date()) let sender = TzKTAddress(alias: fromWallet.walletNickname ?? fromWallet.socialUsername ?? fromWallet.address, address: fromWallet.address) - var transaction = TzKTTransaction(type: .transaction, id: id, level: id, timestamp: timestamp, hash: opHash, counter: counter, initiater: nil, sender: sender, bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: destination, prevDelegate: nil, newDelegate: nil, amount: xtzAmount, parameter: parameters, status: status, hasInternals: false, tokenTransfersCount: nil, errors: nil) + var transaction = TzKTTransaction(type: .transaction, id: id, level: id, timestamp: timestamp, hash: opHash, counter: counter, initiater: nil, sender: sender, bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: destination, prevDelegate: nil, newDelegate: nil, amount: xtzAmount, parameter: parameters, status: status, hasInternals: false, tokenTransfersCount: nil, errors: nil, kind: kind) transaction.processAdditionalData(withCurrentWalletAddress: fromWallet.address) if let pToken = primaryToken { @@ -271,7 +280,7 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident let timestamp = TzKTTransaction.dateFormatter.string(from: Date()) let sender = TzKTAddress(alias: fromWallet.walletNickname ?? fromWallet.socialUsername ?? fromWallet.address, address: fromWallet.address) - var transaction = TzKTTransaction(type: .delegation, id: id, level: id, timestamp: timestamp, hash: opHash, counter: counter, initiater: nil, sender: sender, bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: nil, prevDelegate: nil, newDelegate: newDelegate, amount: .zero(), parameter: nil, status: status, hasInternals: false, tokenTransfersCount: nil, errors: nil) + var transaction = TzKTTransaction(type: .delegation, id: id, level: id, timestamp: timestamp, hash: opHash, counter: counter, initiater: nil, sender: sender, bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: nil, prevDelegate: nil, newDelegate: newDelegate, amount: .zero(), parameter: nil, status: status, hasInternals: false, tokenTransfersCount: nil, errors: nil, kind: nil) transaction.processAdditionalData(withCurrentWalletAddress: fromWallet.address) return transaction @@ -327,6 +336,12 @@ public struct TzKTTransaction: Codable, CustomStringConvertible, Hashable, Ident self.subType = .contractCall self.entrypointCalled = entrypoint + } else if self.kind == "stake" { + self.subType = .stake + + } else if self.kind == "unstake" { + self.subType = .unstake + } else { if self.type == .delegation { self.subType = .delegate diff --git a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift index 05d5366..3865439 100644 --- a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift +++ b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift @@ -59,7 +59,7 @@ class TzKTClientTests: XCTestCase { MockConstants.shared.tzktClient.fetchTransactions(forAddress: MockConstants.defaultHdWallet.address) { transactions in let groups = MockConstants.shared.tzktClient.groupTransactions(transactions: transactions, currentWalletAddress: MockConstants.defaultHdWallet.address) - XCTAssert(groups.count == 19, "\(groups.count)") + XCTAssert(groups.count == 21, "\(groups.count)") for (index, group) in groups.enumerated() { @@ -228,6 +228,20 @@ class TzKTClientTests: XCTestCase { XCTAssert(group.hash == "opC815T6zqTUtzQktPBBeLAB1eRnvuR5ETZDoLPGgAb3698wwFK", group.hash) XCTAssert(group.status == .failed, group.status.rawValue) + case 19: + XCTAssert(group.groupType == .unstake, group.groupType.rawValue) + XCTAssert(group.transactions.count == 1, group.transactions.count.description) + XCTAssert(group.transactions.first?.amount.description == "1", group.transactions.first?.amount.description ?? "-") + XCTAssert(group.hash == "ooyVR1r5vt3K4JGoVnH2XLQjwAVpoZaAkfdG1PssCPPovi7m1FL", group.hash) + XCTAssert(group.status == .applied, group.status.rawValue) + + case 20: + XCTAssert(group.groupType == .stake, group.groupType.rawValue) + XCTAssert(group.transactions.count == 1, group.transactions.count.description) + XCTAssert(group.transactions.first?.amount.description == "10", group.transactions.first?.amount.description ?? "-") + XCTAssert(group.hash == "opPGcuZ459ZGR11RXaL2rRDtKnHFC9o5JQdyBHj3Qua4BMBkAsi", group.hash) + XCTAssert(group.status == .applied, group.status.rawValue) + default: XCTFail("Missing test for transaction") } diff --git a/Tests/KukaiCoreSwiftTests/MockConstants.swift b/Tests/KukaiCoreSwiftTests/MockConstants.swift index 0313995..a61a223 100644 --- a/Tests/KukaiCoreSwiftTests/MockConstants.swift +++ b/Tests/KukaiCoreSwiftTests/MockConstants.swift @@ -57,7 +57,7 @@ public struct MockConstants { bcdTokenBalanceURL.appendQueryItem(name: "hide_empty", value: "true") var tzktHistoryMainURL = tzktURL.appendingPathComponent("v1/accounts/tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss/operations") - tzktHistoryMainURL.appendQueryItem(name: "type", value: "delegation,origination,transaction") + tzktHistoryMainURL.appendQueryItem(name: "type", value: "delegation,origination,transaction,staking") tzktHistoryMainURL.appendQueryItem(name: "micheline", value: "1") tzktHistoryMainURL.appendQueryItem(name: "limit", value: "50") diff --git a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift index ff29eec..6706c0c 100644 --- a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift @@ -26,7 +26,7 @@ final class TzKTTransactionTests: XCTestCase { "value": "{\"to\": \"tz1eDdsp1d27kortm5LuoRqD6aKrdxmTiTSQ\", \"from\": \"tz1Z8E2gMHLXFcGGzW68B5e1Vk49FpHQd7xc\", \"value\": \"500000000000000000\"}" ] - var transaction = TzKTTransaction(type: .transaction, id: 123, level: 123, timestamp: "", hash: "", counter: 123, initiater: TzKTAddress(alias: nil, address: "tz1abc"), sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "KT1GG8Zd5rUp1XV8nMPRBY2tSyVn6NR5F4Q1"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: transferTokenParameters, status: .applied, hasInternals: false, tokenTransfersCount: 0, errors: nil) + var transaction = TzKTTransaction(type: .transaction, id: 123, level: 123, timestamp: "", hash: "", counter: 123, initiater: TzKTAddress(alias: nil, address: "tz1abc"), sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "KT1GG8Zd5rUp1XV8nMPRBY2tSyVn6NR5F4Q1"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: transferTokenParameters, status: .applied, hasInternals: false, tokenTransfersCount: 0, errors: nil, kind: nil) transaction.processAdditionalData(withCurrentWalletAddress: "tz1abc") let token = transaction.getFaTokenTransferData() @@ -42,7 +42,7 @@ final class TzKTTransactionTests: XCTestCase { "entrypoint": "transfer", "value": "{\"to\": \"tz1eDdsp1d27kortm5LuoRqD6aKrdxmTiTSQ\", \"from\": \"tz1Z8E2gMHLXFcGGzW68B5e1Vk49FpHQd7xc\", \"value\": \"500000000000000000\"}" ] - let transaction1 = TzKTTransaction(type: .transaction, id: 1, level: 1, timestamp: "", hash: "", counter: 1, initiater: nil, sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "tz1abc"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: dictParams, status: .confirmed, hasInternals: false, tokenTransfersCount: nil, errors: nil) + let transaction1 = TzKTTransaction(type: .transaction, id: 1, level: 1, timestamp: "", hash: "", counter: 1, initiater: nil, sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "tz1abc"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: dictParams, status: .confirmed, hasInternals: false, tokenTransfersCount: nil, errors: nil, kind: nil) let dict = transaction1.parameterValueAsDict() let dictValue1 = dict?["to"] as? String XCTAssert(dictValue1 == "tz1eDdsp1d27kortm5LuoRqD6aKrdxmTiTSQ", dictValue1 ?? "-") @@ -53,7 +53,7 @@ final class TzKTTransactionTests: XCTestCase { "entrypoint": "transfer", "value": "[\"test\", 123, 14.7]" ] - let transaction2 = TzKTTransaction(type: .transaction, id: 1, level: 1, timestamp: "", hash: "", counter: 1, initiater: nil, sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "tz1abc"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: arrayParams, status: .confirmed, hasInternals: false, tokenTransfersCount: nil, errors: nil) + let transaction2 = TzKTTransaction(type: .transaction, id: 1, level: 1, timestamp: "", hash: "", counter: 1, initiater: nil, sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "tz1abc"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: arrayParams, status: .confirmed, hasInternals: false, tokenTransfersCount: nil, errors: nil, kind: nil) let array = transaction2.parameterValueAsArray() let arrayValue1 = array?[0] as? String let arrayValue2 = array?[1] as? Int @@ -67,7 +67,7 @@ final class TzKTTransactionTests: XCTestCase { "entrypoint": "transfer", "value": "[{\"key\": \"value\"}, {\"key\": 123}, {\"key\": 14.7}]" ] - let transaction3 = TzKTTransaction(type: .transaction, id: 1, level: 1, timestamp: "", hash: "", counter: 1, initiater: nil, sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "tz1abc"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: arrayOfDictParams, status: .confirmed, hasInternals: false, tokenTransfersCount: nil, errors: nil) + let transaction3 = TzKTTransaction(type: .transaction, id: 1, level: 1, timestamp: "", hash: "", counter: 1, initiater: nil, sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "tz1abc"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: arrayOfDictParams, status: .confirmed, hasInternals: false, tokenTransfersCount: nil, errors: nil, kind: nil) let arryOfDict = transaction3.parameterValueAsArrayOfDictionary() let arryOfDictValue1 = (arryOfDict?[0] as? [String: String])?["key"] as? String let arryOfDictValue2 = (arryOfDict?[1] as? [String: Int])?["key"] as? Int @@ -81,7 +81,7 @@ final class TzKTTransactionTests: XCTestCase { let source = WalletMetadata(address: "tz1abc", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) let placeholder1 = TzKTTransaction.placeholder(withStatus: .unconfirmed, id: 567, opHash: "abc123", type: .transaction, counter: 0, fromWallet: source, newDelegate: TzKTAddress(alias: "Baking Benjamins", address: "tz1YgDUQV2eXm8pUWNz3S5aWP86iFzNp4jnD")) - let placeholder2 = TzKTTransaction.placeholder(withStatus: .unconfirmed, id: 456, opHash: "def456", type: .transaction, counter: 1, fromWallet: source, destination: TzKTAddress(alias: nil, address: "tz1def"), xtzAmount: .init(fromNormalisedAmount: 4.17, decimalPlaces: 6), parameters: nil, primaryToken: nil) + let placeholder2 = TzKTTransaction.placeholder(withStatus: .unconfirmed, id: 456, opHash: "def456", type: .transaction, counter: 1, fromWallet: source, destination: TzKTAddress(alias: nil, address: "tz1def"), xtzAmount: .init(fromNormalisedAmount: 4.17, decimalPlaces: 6), parameters: nil, primaryToken: nil, kind: nil) XCTAssert(placeholder1.newDelegate?.address == "tz1YgDUQV2eXm8pUWNz3S5aWP86iFzNp4jnD", placeholder1.newDelegate?.address ?? "-") XCTAssert(placeholder2.amount.description == "4.17", placeholder2.amount.description) @@ -93,7 +93,7 @@ final class TzKTTransactionTests: XCTestCase { "value": "{\"to\": \"tz1eDdsp1d27kortm5LuoRqD6aKrdxmTiTSQ\", \"from\": \"tz1Z8E2gMHLXFcGGzW68B5e1Vk49FpHQd7xc\", \"value\": \"500000000000000000\"}" ] - var transaction = TzKTTransaction(type: .transaction, id: 123, level: 123, timestamp: "", hash: "", counter: 123, initiater: TzKTAddress(alias: nil, address: "tz1abc"), sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "KT1GG8Zd5rUp1XV8nMPRBY2tSyVn6NR5F4Q1"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: transferTokenParameters, status: .applied, hasInternals: false, tokenTransfersCount: 0, errors: nil) + var transaction = TzKTTransaction(type: .transaction, id: 123, level: 123, timestamp: "", hash: "", counter: 123, initiater: TzKTAddress(alias: nil, address: "tz1abc"), sender: TzKTAddress(alias: nil, address: "tz1abc"), bakerFee: .zero(), storageFee: .zero(), allocationFee: .zero(), target: TzKTAddress(alias: nil, address: "KT1GG8Zd5rUp1XV8nMPRBY2tSyVn6NR5F4Q1"), prevDelegate: nil, newDelegate: nil, amount: .zero(), parameter: transferTokenParameters, status: .applied, hasInternals: false, tokenTransfersCount: 0, errors: nil, kind: nil) transaction.processAdditionalData(withCurrentWalletAddress: "tz1abc") if let json = try? JSONEncoder().encode(transaction), let jsonString = String(data: json, encoding: .utf8) { diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_transactions-main.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_transactions-main.json index a6edc35..4159c7c 100644 --- a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_transactions-main.json +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_transactions-main.json @@ -991,5 +991,55 @@ } ], "hasInternals": false + }, + { + "type": "staking", + "id": 314516181090304, + "level": 1672990, + "timestamp": "2024-06-18T12:03:40Z", + "hash": "ooyVR1r5vt3K4JGoVnH2XLQjwAVpoZaAkfdG1PssCPPovi7m1FL", + "sender": { + "address": "tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss" + }, + "counter": 392658, + "gasLimit": 4335, + "gasUsed": 4249, + "storageLimit": 0, + "bakerFee": 705, + "action": "unstake", + "requestedAmount": 1000000, + "amount": 1000000, + "baker": { + "alias": "Baking Benjamins", + "address": "tz1YgDUQV2eXm8pUWNz3S5aWP86iFzNp4jnD" + }, + "stakingUpdatesCount": 1, + "status": "applied", + "kind": "unstake" + }, + { + "type": "staking", + "id": 314486743367680, + "level": 1672989, + "timestamp": "2024-06-18T10:41:46Z", + "hash": "opPGcuZ459ZGR11RXaL2rRDtKnHFC9o5JQdyBHj3Qua4BMBkAsi", + "sender": { + "address": "tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss" + }, + "counter": 392657, + "gasLimit": 3907, + "gasUsed": 3830, + "storageLimit": 0, + "bakerFee": 663, + "action": "stake", + "requestedAmount": 10000000, + "amount": 10000000, + "baker": { + "alias": "Baking Benjamins", + "address": "tz1YgDUQV2eXm8pUWNz3S5aWP86iFzNp4jnD" + }, + "stakingUpdatesCount": 2, + "status": "applied", + "kind": "stake" } ] \ No newline at end of file