From 1e3c41c7693353c189895f1fb0dcbb749a6ca0f5 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 20 Jun 2024 15:23:26 +0100 Subject: [PATCH 01/42] add unit tests for baking rewards with no previous payout to test early users, catching the previous issue --- .../Clients/TzKTClientTests.swift | 28 ++ Tests/KukaiCoreSwiftTests/MockConstants.swift | 14 + ...-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json | 139 ++++++++ .../tzkt_delegator-rewards-no-previous.json | 314 ++++++++++++++++++ 4 files changed, 495 insertions(+) create mode 100644 Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json create mode 100644 Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-no-previous.json diff --git a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift index fcf063b..f4f8b21 100644 --- a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift +++ b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift @@ -434,6 +434,34 @@ class TzKTClientTests: XCTestCase { wait(for: [expectation], timeout: 120) } + func testEstimateRewardsNoPrevious() { + let expectation = XCTestExpectation(description: "tzkt-testEstimateRewardsNoPrevious") + let delegate = TzKTAccountDelegate(alias: "The Shire", address: "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", active: true) + + MockConstants.shared.tzktClient.estimateLastAndNextReward(forAddress: "tz1iv8r8UUCEZK5gqpLPnMPzP4VRJBJUdGgr", delegate: delegate) { result in + switch result { + case .success(let rewards): + XCTAssert(rewards.previousReward == nil, rewards.previousReward?.amount.description ?? "") + + XCTAssert(rewards.estimatedPreviousReward == nil, rewards.estimatedPreviousReward?.amount.normalisedRepresentation ?? "") + + XCTAssert(rewards.estimatedNextReward?.amount.normalisedRepresentation == "0.000368", rewards.estimatedNextReward?.amount.normalisedRepresentation ?? "") + XCTAssert(rewards.estimatedNextReward?.fee.description == "0.042", rewards.estimatedNextReward?.fee.description ?? "") + XCTAssert(rewards.estimatedNextReward?.cycle.description == "743", rewards.estimatedNextReward?.cycle.description ?? "") + XCTAssert(rewards.estimatedNextReward?.bakerAlias == "The Shire", rewards.estimatedNextReward?.bakerAlias ?? "") + + XCTAssert(rewards.moreThan1CycleBetweenPreiousAndNext() == false) + + case .failure(let error): + XCTFail("Error: \(error)") + } + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 120) + } + func testBakers() { let expectation = XCTestExpectation(description: "tzkt-bakers") diff --git a/Tests/KukaiCoreSwiftTests/MockConstants.swift b/Tests/KukaiCoreSwiftTests/MockConstants.swift index a61a223..0ceff99 100644 --- a/Tests/KukaiCoreSwiftTests/MockConstants.swift +++ b/Tests/KukaiCoreSwiftTests/MockConstants.swift @@ -90,6 +90,9 @@ public struct MockConstants { var tzktDelegatorRewardsURL = tzktURL.appendingPathComponent("v1/rewards/delegators/tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss") tzktDelegatorRewardsURL.appendQueryItem(name: "limit", value: 25) + var tzktDelegatorRewardsNoPreviousURL = tzktURL.appendingPathComponent("v1/rewards/delegators/tz1iv8r8UUCEZK5gqpLPnMPzP4VRJBJUdGgr") + tzktDelegatorRewardsNoPreviousURL.appendQueryItem(name: "limit", value: 25) + var bakingBadConfigURL1 = bakingBadURL.appendingPathComponent("v2/bakers/tz1fwnfJNgiDACshK9avfRfFbMaXrs3ghoJa") bakingBadConfigURL1.appendQueryItem(name: "configs", value: "true") @@ -99,6 +102,9 @@ public struct MockConstants { var bakingBadConfigURL3 = bakingBadURL.appendingPathComponent("v2/bakers/tz1S5WxdZR5f9NzsPXhr7L9L1vrEb5spZFur") bakingBadConfigURL3.appendQueryItem(name: "configs", value: "true") + var bakingBadConfigURL4 = bakingBadURL.appendingPathComponent("v2/bakers/tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM") + bakingBadConfigURL4.appendQueryItem(name: "configs", value: "true") + var tzktsuggestURL1 = tzktURL.appendingPathComponent("v1/suggest/accounts/Bake Nug Payouts") tzktsuggestURL1.appendQueryItem(name: "limit", value: 1) @@ -126,6 +132,11 @@ public struct MockConstants { tzktLastBakerRewardURL3.appendQueryItem(name: "type", value: "transaction") tzktLastBakerRewardURL3.appendQueryItem(name: "sender.in", value: "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv,tz1S5WxdZR5f9NzsPXhr7L9L1vrEb5spZFur,tz1gnuBF9TbBcgHPV2mUE96tBrW7PxqRmx1h") + var tzktLastBakerRewardURL4 = tzktURL.appendingPathComponent("v1/accounts/tz1iv8r8UUCEZK5gqpLPnMPzP4VRJBJUdGgr/operations") + tzktLastBakerRewardURL4.appendQueryItem(name: "limit", value: 1) + tzktLastBakerRewardURL4.appendQueryItem(name: "type", value: "transaction") + tzktLastBakerRewardURL4.appendQueryItem(name: "sender.in", value: "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv,tz1ShireJgwr8ag5dETMY4RNqkXeu1YgyDYC") + var tzktDelegatesURL = tzktURL.appendingPathComponent("v1/delegates") tzktDelegatesURL.appendQueryItem(name: "select.values", value: "address,alias,balance,stakingBalance") tzktDelegatesURL.appendQueryItem(name: "active", value: "true") @@ -179,9 +190,11 @@ public struct MockConstants { tzktBalancePageURL: (MockConstants.jsonStub(fromFilename: "tzkt_balance-page"), MockConstants.http200), tzktCyclesURL: (MockConstants.jsonStub(fromFilename: "tzkt_cycles"), MockConstants.http200), tzktDelegatorRewardsURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards"), MockConstants.http200), + tzktDelegatorRewardsNoPreviousURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards-no-previous"), MockConstants.http200), bakingBadConfigURL1: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1fwnfJNgiDACshK9avfRfFbMaXrs3ghoJa"), MockConstants.http200), bakingBadConfigURL2: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv"), MockConstants.http200), bakingBadConfigURL3: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1S5WxdZR5f9NzsPXhr7L9L1vrEb5spZFur"), MockConstants.http200), + bakingBadConfigURL4: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM"), MockConstants.http200), tzktsuggestURL1: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-bake-nug"), MockConstants.http200), tzktsuggestURL2: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-the-shire"), MockConstants.http200), tzktsuggestURL3: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-the-shire_updated"), MockConstants.http200), @@ -189,6 +202,7 @@ public struct MockConstants { tzktLastBakerRewardURL: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment"), MockConstants.http200), tzktLastBakerRewardURL2: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment"), MockConstants.http200), tzktLastBakerRewardURL3: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), + tzktLastBakerRewardURL4: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), tzktDelegatesURL: (MockConstants.jsonStub(fromFilename: "tzkt_ghostnet-bakers"), MockConstants.http200), // Media proxy diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json new file mode 100644 index 0000000..3592d89 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM.json @@ -0,0 +1,139 @@ +{ + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", + "name": "The Shire", + "logo": "https://services.tzkt.io/v1/avatars/tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", + "balance": 5326177.599918, + "stakingBalance": 44473420.298689, + "stakingCapacity": 53261775.99918, + "maxStakingBalance": 53261775.99918, + "freeSpace": 8788355.700490996, + "fee": 0.15, + "minDelegation": 0, + "payoutDelay": 1, + "payoutPeriod": 1, + "openForDelegation": true, + "estimatedRoi": 0.053113, + "serviceType": "multiasset", + "serviceHealth": "active", + "payoutTiming": "stable", + "payoutAccuracy": "precise", + "audit": "601aabbe5e0a85c68fb18d13", + "insuranceCoverage": 0, + "config": { + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv", + "fee": [ + { + "cycle": 559, + "value": 0.15 + }, + { + "cycle": 473, + "value": 0.109 + }, + { + "cycle": 468, + "value": 0.05 + }, + { + "cycle": 440, + "value": 0.109 + }, + { + "cycle": 437, + "value": 0.12 + }, + { + "cycle": 407, + "value": 0.15 + }, + { + "cycle": 265, + "value": 0.08 + }, + { + "cycle": 0, + "value": 0.05 + } + ], + "minDelegation": [ + { + "cycle": 0, + "value": 0 + } + ], + "payoutFee": [ + { + "cycle": 0, + "value": false + } + ], + "payoutDelay": [ + { + "cycle": 467, + "value": 1 + }, + { + "cycle": 466, + "value": 2 + }, + { + "cycle": 465, + "value": 3 + }, + { + "cycle": 464, + "value": 4 + }, + { + "cycle": 463, + "value": 5 + }, + { + "cycle": 0, + "value": 6 + } + ], + "payoutPeriod": [ + { + "cycle": 0, + "value": 1 + } + ], + "minPayout": [ + { + "cycle": 0, + "value": 0.001 + } + ], + "rewardStruct": [ + { + "cycle": 0, + "value": 981 + } + ], + "payoutRatio": [ + { + "cycle": 0, + "value": 666 + } + ], + "maxStakingThreshold": [ + { + "cycle": 0, + "value": 1 + } + ], + "openForDelegation": [ + { + "cycle": 0, + "value": true + } + ], + "allocationFee": [ + { + "cycle": 0, + "value": false + } + ] + } + } \ No newline at end of file diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-no-previous.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-no-previous.json new file mode 100644 index 0000000..f902711 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-no-previous.json @@ -0,0 +1,314 @@ +[ + { + "cycle": 750, + "delegatedBalance": 452228, + "stakedBalance": 0, + "baker": { + "alias": "The Shire", + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv" + }, + "bakingPower": 44411942965285, + "totalBakingPower": 670376512904615, + "bakerDelegatedBalance": 973715068775, + "externalDelegatedBalance": 39035845408189, + "bakerStakedBalance": 4445506719663, + "externalStakedBalance": 0, + "expectedBlocks": 1628, + "expectedEndorsements": 11396991, + "futureBlocks": 1601, + "futureBlockRewards": 10670436057, + "blocks": 0, + "blockRewardsDelegated": 0, + "blockRewardsStakedOwn": 0, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 0, + "missedBlockRewards": 0, + "futureEndorsements": 11394754, + "futureEndorsementRewards": 10849935432, + "endorsements": 0, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 0, + "missedEndorsementRewards": 0, + "blockFees": 0, + "missedBlockFees": 0, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 0, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 0, + "stakingBalance": 44455067196627, + "activeStake": 44411942965285, + "selectedStake": 670376512904615, + "balance": 452228, + "ownBlocks": 0, + "extraBlocks": 0, + "missedOwnBlocks": 0, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 0, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 0, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 0, + "extraBlockFees": 0, + "missedOwnBlockFees": 0, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + }, + { + "cycle": 749, + "delegatedBalance": 629661, + "stakedBalance": 0, + "baker": { + "alias": "The Shire", + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv" + }, + "bakingPower": 44471812616608, + "totalBakingPower": 672634125017457, + "bakerDelegatedBalance": 946051522616, + "externalDelegatedBalance": 39089076546183, + "bakerStakedBalance": 4448347563200, + "externalStakedBalance": 0, + "expectedBlocks": 1624, + "expectedEndorsements": 11374051, + "futureBlocks": 1656, + "futureBlockRewards": 11037003192, + "blocks": 0, + "blockRewardsDelegated": 0, + "blockRewardsStakedOwn": 0, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 0, + "missedBlockRewards": 0, + "futureEndorsements": 11375422, + "futureEndorsementRewards": 10828096552, + "endorsements": 0, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 0, + "missedEndorsementRewards": 0, + "blockFees": 0, + "missedBlockFees": 0, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 0, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 0, + "stakingBalance": 44483475631999, + "activeStake": 44471812616608, + "selectedStake": 672634125017457, + "balance": 629661, + "ownBlocks": 0, + "extraBlocks": 0, + "missedOwnBlocks": 0, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 0, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 0, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 0, + "extraBlockFees": 0, + "missedOwnBlockFees": 0, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + }, + { + "cycle": 748, + "delegatedBalance": 799354, + "stakedBalance": 0, + "baker": { + "alias": "The Shire", + "address": "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv" + }, + "bakingPower": 44444439841760, + "totalBakingPower": 672533812378748, + "bakerDelegatedBalance": 922361570575, + "externalDelegatedBalance": 39110426817844, + "bakerStakedBalance": 4448087598714, + "externalStakedBalance": 0, + "expectedBlocks": 1624, + "expectedEndorsements": 11368745, + "futureBlocks": 954, + "futureBlockRewards": 6358273578, + "blocks": 659, + "blockRewardsDelegated": 3849280033, + "blockRewardsStakedOwn": 428088086, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 2, + "missedBlockRewards": 12321546, + "futureEndorsements": 6992818, + "futureEndorsementRewards": 10823045240, + "endorsements": 4366738, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 8359, + "missedEndorsementRewards": 0, + "blockFees": 8503447, + "missedBlockFees": 55543, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 3849280033, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 4277368119, + "stakingBalance": 44480875987133, + "activeStake": 44444439841760, + "selectedStake": 672533812378748, + "balance": 799354, + "ownBlocks": 659, + "extraBlocks": 0, + "missedOwnBlocks": 2, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 4277368119, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 12321546, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 8503447, + "extraBlockFees": 0, + "missedOwnBlockFees": 55543, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + } +] \ No newline at end of file From 1434c6da0436ddeea18c9537b70b84a968a66e65 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Fri, 21 Jun 2024 10:40:24 +0100 Subject: [PATCH 02/42] watch wallet cache should fail if the wallet exists anywhere in the list as it will cause confusion to allow it to appear in both lists --- .../Services/WalletCacheService.swift | 4 ++-- .../Services/WalletCacheServiceTests.swift | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 65e7f9f..466407e 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -143,8 +143,8 @@ public class WalletCacheService { public func cacheWatchWallet(metadata: WalletMetadata) throws { var list = readMetadataFromDiskAndDecrypt() - if let _ = list.watchWallets.first(where: { $0.address == metadata.address }) { - Logger.walletCache.error("cacheWatchWallet - Unable to cache wallet, walelt already exists") + if let _ = list.addresses().first(where: { $0 == metadata.address }) { + Logger.walletCache.error("cacheWatchWallet - Unable to cache wallet, wallet already exists") throw WalletCacheError.walletAlreadyExists } diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index 7e19da8..c295fa4 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -345,4 +345,23 @@ class WalletCacheServiceTests: XCTestCase { XCTFail("Should not error: \(error)") } } + + func testWatchWalletThatAlreadyExists() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + do { + try walletCacheService.cache(wallet: MockConstants.defaultLinearWallet, childOfIndex: nil, backedUp: false) + } catch { + XCTFail("Should not error: \(error)") + } + + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + + do { + try walletCacheService.cacheWatchWallet(metadata: watchWallet) + XCTFail("Already exists, should be rejected") + } catch let error { + XCTAssert(error.localizedDescription == "The operation couldn’t be completed. (KukaiCoreSwift.WalletCacheError error 8.)", error.localizedDescription) + } + } } From 922e83b63c68ac275182ae2df5dbf7ce0c4a3ae9 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Fri, 21 Jun 2024 10:56:47 +0100 Subject: [PATCH 03/42] importing a wallet already being watched, should remove the watched wallet, as it will cause confusion in the code --- .../Services/WalletCacheService.swift | 9 ++++++ .../Services/WalletCacheServiceTests.swift | 29 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 466407e..97c6749 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -135,8 +135,17 @@ public class WalletCacheService { if encryptAndWriteWalletsToDisk(wallets: newWallets) && encryptAndWriteMetadataToDisk(newMetadata) == false { throw WalletCacheError.unableToEncryptAndWrite + } else { + removeNewAddressFromWatchListIfExists(wallet.address, list: newMetadata) } } + + private func removeNewAddressFromWatchListIfExists(_ address: String, list: WalletMetadataList) { + if let _ = list.watchWallets.first(where: { $0.address == address }) { + let _ = deleteWatchWallet(address: address) + } + } + /** Cahce a watch wallet metadata obj, only. Metadata cahcing handled via wallet cache method */ diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index c295fa4..6b211d9 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -338,7 +338,7 @@ class WalletCacheServiceTests: XCTestCase { do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) - let list = walletCacheService.readMetadataFromDiskAndDecrypt() + let list = walletCacheService.readMetadataFromDiskAndDecrypt() let watch = list.watchWallets XCTAssert(watch.count == 1, watch.count.description) } catch { @@ -364,4 +364,31 @@ class WalletCacheServiceTests: XCTestCase { XCTAssert(error.localizedDescription == "The operation couldn’t be completed. (KukaiCoreSwift.WalletCacheError error 8.)", error.localizedDescription) } } + + func testWatchWalletRemovedAfterImported() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + // Add watch and confirm + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + + do { + try walletCacheService.cacheWatchWallet(metadata: watchWallet) + let list = walletCacheService.readMetadataFromDiskAndDecrypt() + let watch = list.watchWallets + XCTAssert(watch.count == 1, watch.count.description) + } catch let error { + XCTFail("Should not error: \(error)") + } + + + // Add the wallet via an import, test watch is now been removed + do { + try walletCacheService.cache(wallet: MockConstants.defaultLinearWallet, childOfIndex: nil, backedUp: false) + let list = walletCacheService.readMetadataFromDiskAndDecrypt() + let watch = list.watchWallets + XCTAssert(watch.count == 0, watch.count.description) + } catch { + XCTFail("Should not error: \(error)") + } + } } From b5f65d9e8f9a4041f74a7265be3e2e3a07212148 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 26 Jun 2024 11:03:41 +0100 Subject: [PATCH 04/42] bump temporary cache up to 30 days --- Sources/KukaiCoreSwift/Services/MediaProxyService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/MediaProxyService.swift b/Sources/KukaiCoreSwift/Services/MediaProxyService.swift index 39e024b..8c980c6 100644 --- a/Sources/KukaiCoreSwift/Services/MediaProxyService.swift +++ b/Sources/KukaiCoreSwift/Services/MediaProxyService.swift @@ -96,7 +96,7 @@ public class MediaProxyService: NSObject { public static func setupImageLibrary() { MediaProxyService.permanentCache.config.maxMemoryCost = UInt(100 * 1000 * 1000) // 100 MB - MediaProxyService.temporaryCache.config.maxDiskAge = 3600 * 24 * 7 // 1 Week + MediaProxyService.temporaryCache.config.maxDiskAge = 3600 * 24 * 30 // 30 days MediaProxyService.temporaryCache.config.maxMemoryCost = UInt(500 * 1000 * 1000) // 500 MB MediaProxyService.detailCache.config.maxDiskAge = 3600 * 24 // 1 day From 40d7e182db543126789763ebc87527c57f71a1da Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 26 Jun 2024 14:54:23 +0100 Subject: [PATCH 05/42] - change testnet networktype to ghostnet - add a new network type for protocolnet, update references and logic - bump sdwebimage cache --- Package.swift | 2 +- .../Clients/TezosDomainsClient.swift | 4 ++-- .../Models/Config/TezosNodeClientConfig.swift | 20 +++++++++++-------- .../Services/TorusAuthService.swift | 10 +++++----- Tests/KukaiCoreSwiftTests/MockConstants.swift | 2 +- .../Config/TezosNodeClientConfigTests.swift | 2 +- .../Services/WalletCacheServiceTests.swift | 4 ++-- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Package.swift b/Package.swift index 05472fb..bd8e6b0 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( .package(name: "KukaiCryptoSwift", url: "https://github.com/kukai-wallet/kukai-crypto-swift", from: "1.0.23" /*.branch("develop")*/), .package(name: "CustomAuth", url: "https://github.com/torusresearch/customauth-swift-sdk", from: "10.0.1"), .package(name: "SignalRClient", url: "https://github.com/moozzyk/SignalR-Client-Swift", from: "0.8.0"), - .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.18.10") + .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.19.3") ], targets: [ .target( diff --git a/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift b/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift index 9229823..e11aa5f 100644 --- a/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TezosDomainsClient.swift @@ -86,7 +86,7 @@ public class TezosDomainsClient { dispatchGroup.leave() } - getDomainFor(address: address, url: TezosNodeClientConfig.defaultTestnetURLs.tezosDomainsURL) { result in + getDomainFor(address: address, url: TezosNodeClientConfig.defaultGhostnetURLs.tezosDomainsURL) { result in guard let res = try? result.get() else { errorGhost = result.getFailure() dispatchGroup.leave() @@ -172,7 +172,7 @@ public class TezosDomainsClient { dispatchGroup.leave() } - getDomainsFor(addresses: addresses, url: TezosNodeClientConfig.defaultTestnetURLs.tezosDomainsURL) { result in + getDomainsFor(addresses: addresses, url: TezosNodeClientConfig.defaultGhostnetURLs.tezosDomainsURL) { result in guard let res = try? result.get() else { errorGhost = result.getFailure() dispatchGroup.leave() diff --git a/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift b/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift index cb8cc14..891e7ef 100644 --- a/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift +++ b/Sources/KukaiCoreSwift/Models/Config/TezosNodeClientConfig.swift @@ -16,7 +16,8 @@ public struct TezosNodeClientConfig { /// An enum indicating whether the network is mainnet or testnet public enum NetworkType: String { case mainnet - case testnet + case ghostnet + case protocolnet } /// Allow switching between local forging or remote forging+parsing @@ -49,7 +50,7 @@ public struct TezosNodeClientConfig { } /// Preconfigured struct with all the URL's needed to work with Tezos testnet - public struct defaultTestnetURLs { + public struct defaultGhostnetURLs { /// The default testnet URLs to use for estimating and injecting operations public static let nodeURLs = [URL(string: "https://ghostnet.smartpy.io")!, URL(string: "https://rpc.ghostnet.tzboot.net")!] @@ -141,13 +142,16 @@ public struct TezosNodeClientConfig { tezosDomainsURL = TezosNodeClientConfig.defaultMainnetURLs.tezosDomainsURL objktApiURL = TezosNodeClientConfig.defaultMainnetURLs.objktApiURL - case .testnet: - nodeURLs = TezosNodeClientConfig.defaultTestnetURLs.nodeURLs + case .ghostnet: + nodeURLs = TezosNodeClientConfig.defaultGhostnetURLs.nodeURLs forgingType = .local - tzktURL = TezosNodeClientConfig.defaultTestnetURLs.tzktURL - betterCallDevURL = TezosNodeClientConfig.defaultTestnetURLs.betterCallDevURL - tezosDomainsURL = TezosNodeClientConfig.defaultTestnetURLs.tezosDomainsURL - objktApiURL = TezosNodeClientConfig.defaultTestnetURLs.objktApiURL + tzktURL = TezosNodeClientConfig.defaultGhostnetURLs.tzktURL + betterCallDevURL = TezosNodeClientConfig.defaultGhostnetURLs.betterCallDevURL + tezosDomainsURL = TezosNodeClientConfig.defaultGhostnetURLs.tezosDomainsURL + objktApiURL = TezosNodeClientConfig.defaultGhostnetURLs.objktApiURL + + case .protocolnet: + fatalError("No defaults for networkType protocolnet. Must be user supplied") } } diff --git a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift index 9704d11..ac2f966 100644 --- a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift +++ b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift @@ -161,10 +161,10 @@ public class TorusAuthService: NSObject { aggregateVerifierType: verifierWrapper.verifierType, aggregateVerifier: verifierWrapper.aggregateVerifierName ?? verifierWrapper.subverifier.verifier, subVerifierDetails: [verifierWrapper.subverifier], - network: verifierWrapper.networkType == .testnet ? .legacy(.TESTNET) : .legacy(.MAINNET), + network: verifierWrapper.networkType == .mainnet ? .legacy(.MAINNET) : .legacy(.TESTNET), loglevel: .error, urlSession: self.networkService.urlSession, - networkUrl: verifierWrapper.networkType == .testnet ? "https://www.ankr.com/rpc/eth/eth_goerli" : nil) + networkUrl: verifierWrapper.networkType == .mainnet ? nil : "https://www.ankr.com/rpc/eth/eth_goerli") } @@ -317,7 +317,7 @@ public class TorusAuthService: NSObject { /// Private wrapper to avoid duplication in the previous function private func getPublicAddress(verifierName: String, verifierWrapper: SubverifierWrapper, socialUserId: String, completion: @escaping ((Result) -> Void)) { - let isTestnet = (verifierWrapper.networkType == .testnet) + let isTestnet = (verifierWrapper.networkType != .mainnet) self.fetchNodeDetails = NodeDetailManager(network: (isTestnet ? .legacy(.TESTNET) : .legacy(.MAINNET)), urlSession: networkService.urlSession) Task { @@ -457,10 +457,10 @@ extension TorusAuthService: ASAuthorizationControllerDelegate, ASAuthorizationCo aggregateVerifierType: verifierWrapper.verifierType, aggregateVerifier: verifierWrapper.aggregateVerifierName ?? verifierWrapper.subverifier.verifier, subVerifierDetails: [verifierWrapper.subverifier], - network: verifierWrapper.networkType == .testnet ? .legacy(.TESTNET) : .legacy(.MAINNET), + network: verifierWrapper.networkType == .mainnet ? .legacy(.MAINNET) : .legacy(.TESTNET), loglevel: .error, urlSession: self.networkService.urlSession, - networkUrl: verifierWrapper.networkType == .testnet ? "https://www.ankr.com/rpc/eth/eth_goerli" : nil) + networkUrl: verifierWrapper.networkType == .mainnet ? nil : "https://www.ankr.com/rpc/eth/eth_goerli") Task { @MainActor in do { diff --git a/Tests/KukaiCoreSwiftTests/MockConstants.swift b/Tests/KukaiCoreSwiftTests/MockConstants.swift index 0ceff99..1fcc2b9 100644 --- a/Tests/KukaiCoreSwiftTests/MockConstants.swift +++ b/Tests/KukaiCoreSwiftTests/MockConstants.swift @@ -36,7 +36,7 @@ public struct MockConstants { // MARK: - Init private init() { - config = TezosNodeClientConfig(withDefaultsForNetworkType: .testnet) + config = TezosNodeClientConfig(withDefaultsForNetworkType: .ghostnet) loggingConfig = LoggingConfig(logNetworkFailures: true, logNetworkSuccesses: true) let sessionConfig = URLSessionConfiguration.ephemeral // Uses no caching / storage diff --git a/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift b/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift index 63bc6a0..e775e23 100644 --- a/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/Config/TezosNodeClientConfigTests.swift @@ -20,7 +20,7 @@ class TezosNodeClientConfigTests: XCTestCase { } func testDefaults() { - let config1 = TezosNodeClientConfig(withDefaultsForNetworkType: .testnet) + let config1 = TezosNodeClientConfig(withDefaultsForNetworkType: .ghostnet) XCTAssert(config1.nodeURLs[0].absoluteString == "https://ghostnet.smartpy.io", config1.nodeURLs[0].absoluteString) XCTAssert(config1.nodeURLs[1].absoluteString == "https://rpc.ghostnet.tzboot.net", config1.nodeURLs[1].absoluteString) XCTAssert(config1.betterCallDevURL.absoluteString == "https://api.better-call.dev/", config1.betterCallDevURL.absoluteString) diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index 6b211d9..58ebf09 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -255,13 +255,13 @@ class WalletCacheServiceTests: XCTestCase { XCTAssert(metadata1.hasMainnetDomain()) XCTAssert(metadata1.hasGhostnetDomain()) XCTAssert(metadata1.hasDomain(onNetwork: TezosNodeClientConfig.NetworkType.mainnet)) - XCTAssert(metadata1.hasDomain(onNetwork: TezosNodeClientConfig.NetworkType.testnet)) + XCTAssert(metadata1.hasDomain(onNetwork: TezosNodeClientConfig.NetworkType.ghostnet)) XCTAssert(metadata1.mainnetDomains?.first?.domain.name == "blah.tez") XCTAssert(metadata1.ghostnetDomains?.first?.domain.name == "blah.gho") XCTAssert(metadata1.primaryMainnetDomain()?.domain.name == "blah.tez") XCTAssert(metadata1.primaryGhostnetDomain()?.domain.name == "blah.gho") XCTAssert(metadata1.primaryDomain(onNetwork: .mainnet)?.domain.name == "blah.tez") - XCTAssert(metadata1.primaryDomain(onNetwork: .testnet)?.domain.name == "blah.gho") + XCTAssert(metadata1.primaryDomain(onNetwork: .ghostnet)?.domain.name == "blah.gho") From f1b5a0cbac1b55fa78813f697f13b3f2f700cfae Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 26 Jun 2024 15:30:17 +0100 Subject: [PATCH 06/42] - bug fix: baking rewards index out of bounds for new users - add another unit test case to cover this --- .../KukaiCoreSwift/Clients/TzKTClient.swift | 3 +- .../Clients/TzKTClientTests.swift | 28 +++++ Tests/KukaiCoreSwiftTests/MockConstants.swift | 18 +++ ...-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json | 21 ++++ .../Stubs/tzkt_delegator-rewards-none.json | 106 ++++++++++++++++++ .../Stubs/tzkt_suggest-teztillery.json | 6 + 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json create mode 100644 Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-none.json create mode 100644 Tests/KukaiCoreSwiftTests/Stubs/tzkt_suggest-teztillery.json diff --git a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift index 7aee51c..431512f 100644 --- a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift @@ -322,7 +322,8 @@ public class TzKTClient { var estimatedNextReward: RewardDetails? = nil // Check if we have enough rewards to bring us up to the current cycle - guard currentDelegatorRewards.count > TzKTClient.numberOfFutureCyclesReturned, + guard TzKTClient.numberOfFutureCyclesReturned >= 0, + currentDelegatorRewards.count > TzKTClient.numberOfFutureCyclesReturned, let currentBakerConfig = bakerConfigs[delegate.address], let inProgresCycleBakerConfig = bakerConfigs[currentDelegatorRewards[TzKTClient.numberOfFutureCyclesReturned].baker.address] else { diff --git a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift index f4f8b21..87c22b1 100644 --- a/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift +++ b/Tests/KukaiCoreSwiftTests/Clients/TzKTClientTests.swift @@ -462,6 +462,34 @@ class TzKTClientTests: XCTestCase { wait(for: [expectation], timeout: 120) } + func testEstimateRewardsNone() { + let expectation = XCTestExpectation(description: "tzkt-testEstimateRewardsNone") + let delegate = TzKTAccountDelegate(alias: "Teztillery", address: "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4", active: true) + + MockConstants.shared.tzktClient.estimateLastAndNextReward(forAddress: "tz1ckwbvP7pdTLS1aAe6YPoiKpG2d8ENU8Ac", delegate: delegate) { result in + switch result { + case .success(let rewards): + XCTAssert(rewards.previousReward == nil, rewards.previousReward?.amount.description ?? "") + + XCTAssert(rewards.estimatedPreviousReward == nil, rewards.estimatedPreviousReward?.amount.normalisedRepresentation ?? "") + + XCTAssert(rewards.estimatedNextReward?.amount.normalisedRepresentation == "0", rewards.estimatedNextReward?.amount.normalisedRepresentation ?? "") + XCTAssert(rewards.estimatedNextReward?.fee.description == "0.0499", rewards.estimatedNextReward?.fee.description ?? "") + XCTAssert(rewards.estimatedNextReward?.cycle.description == "745", rewards.estimatedNextReward?.cycle.description ?? "") + XCTAssert(rewards.estimatedNextReward?.bakerAlias == "Teztillery", rewards.estimatedNextReward?.bakerAlias ?? "") + + XCTAssert(rewards.moreThan1CycleBetweenPreiousAndNext() == false) + + case .failure(let error): + XCTFail("Error: \(error)") + } + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 120) + } + func testBakers() { let expectation = XCTestExpectation(description: "tzkt-bakers") diff --git a/Tests/KukaiCoreSwiftTests/MockConstants.swift b/Tests/KukaiCoreSwiftTests/MockConstants.swift index 1fcc2b9..c550eca 100644 --- a/Tests/KukaiCoreSwiftTests/MockConstants.swift +++ b/Tests/KukaiCoreSwiftTests/MockConstants.swift @@ -93,6 +93,9 @@ public struct MockConstants { var tzktDelegatorRewardsNoPreviousURL = tzktURL.appendingPathComponent("v1/rewards/delegators/tz1iv8r8UUCEZK5gqpLPnMPzP4VRJBJUdGgr") tzktDelegatorRewardsNoPreviousURL.appendQueryItem(name: "limit", value: 25) + var tzktDelegatorRewardsNoneURL = tzktURL.appendingPathComponent("v1/rewards/delegators/tz1ckwbvP7pdTLS1aAe6YPoiKpG2d8ENU8Ac") + tzktDelegatorRewardsNoneURL.appendQueryItem(name: "limit", value: 25) + var bakingBadConfigURL1 = bakingBadURL.appendingPathComponent("v2/bakers/tz1fwnfJNgiDACshK9avfRfFbMaXrs3ghoJa") bakingBadConfigURL1.appendQueryItem(name: "configs", value: "true") @@ -105,6 +108,9 @@ public struct MockConstants { var bakingBadConfigURL4 = bakingBadURL.appendingPathComponent("v2/bakers/tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM") bakingBadConfigURL4.appendQueryItem(name: "configs", value: "true") + var bakingBadConfigURL5 = bakingBadURL.appendingPathComponent("v2/bakers/tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4") + bakingBadConfigURL5.appendQueryItem(name: "configs", value: "true") + var tzktsuggestURL1 = tzktURL.appendingPathComponent("v1/suggest/accounts/Bake Nug Payouts") tzktsuggestURL1.appendQueryItem(name: "limit", value: 1) @@ -117,6 +123,9 @@ public struct MockConstants { var tzktsuggestURL4 = tzktURL.appendingPathComponent("v1/suggest/accounts/Baking Benjamins Payouts") tzktsuggestURL4.appendQueryItem(name: "limit", value: 1) + var tzktsuggestURL5 = tzktURL.appendingPathComponent("v1/suggest/accounts/Teztillery Payouts") + tzktsuggestURL5.appendQueryItem(name: "limit", value: 1) + var tzktLastBakerRewardURL = tzktURL.appendingPathComponent("v1/accounts/tz1Ue76bLW7boAcJEZf2kSGcamdBKVi4Kpss/operations") tzktLastBakerRewardURL.appendQueryItem(name: "limit", value: 1) tzktLastBakerRewardURL.appendQueryItem(name: "type", value: "transaction") @@ -137,6 +146,11 @@ public struct MockConstants { tzktLastBakerRewardURL4.appendQueryItem(name: "type", value: "transaction") tzktLastBakerRewardURL4.appendQueryItem(name: "sender.in", value: "tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv,tz1ShireJgwr8ag5dETMY4RNqkXeu1YgyDYC") + var tzktLastBakerRewardURL5 = tzktURL.appendingPathComponent("v1/accounts/tz1ckwbvP7pdTLS1aAe6YPoiKpG2d8ENU8Ac/operations") + tzktLastBakerRewardURL5.appendQueryItem(name: "limit", value: 1) + tzktLastBakerRewardURL5.appendQueryItem(name: "type", value: "transaction") + tzktLastBakerRewardURL5.appendQueryItem(name: "sender.in", value: "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4") + var tzktDelegatesURL = tzktURL.appendingPathComponent("v1/delegates") tzktDelegatesURL.appendQueryItem(name: "select.values", value: "address,alias,balance,stakingBalance") tzktDelegatesURL.appendQueryItem(name: "active", value: "true") @@ -191,18 +205,22 @@ public struct MockConstants { tzktCyclesURL: (MockConstants.jsonStub(fromFilename: "tzkt_cycles"), MockConstants.http200), tzktDelegatorRewardsURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards"), MockConstants.http200), tzktDelegatorRewardsNoPreviousURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards-no-previous"), MockConstants.http200), + tzktDelegatorRewardsNoneURL: (MockConstants.jsonStub(fromFilename: "tzkt_delegator-rewards-none"), MockConstants.http200), bakingBadConfigURL1: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1fwnfJNgiDACshK9avfRfFbMaXrs3ghoJa"), MockConstants.http200), bakingBadConfigURL2: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1ZgkTFmiwddPXGbs4yc6NWdH4gELW7wsnv"), MockConstants.http200), bakingBadConfigURL3: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1S5WxdZR5f9NzsPXhr7L9L1vrEb5spZFur"), MockConstants.http200), bakingBadConfigURL4: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1aRoaRhSpRYvFdyvgWLL6TGyRoGF51wDjM"), MockConstants.http200), + bakingBadConfigURL5: (MockConstants.jsonStub(fromFilename: "tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4"), MockConstants.http200), tzktsuggestURL1: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-bake-nug"), MockConstants.http200), tzktsuggestURL2: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-the-shire"), MockConstants.http200), tzktsuggestURL3: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-the-shire_updated"), MockConstants.http200), tzktsuggestURL4: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-baking-benjamins"), MockConstants.http200), + tzktsuggestURL5: (MockConstants.jsonStub(fromFilename: "tzkt_suggest-teztillery"), MockConstants.http200), tzktLastBakerRewardURL: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment"), MockConstants.http200), tzktLastBakerRewardURL2: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment"), MockConstants.http200), tzktLastBakerRewardURL3: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), tzktLastBakerRewardURL4: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), + tzktLastBakerRewardURL5: (MockConstants.jsonStub(fromFilename: "tzkt_last-baker-payment_updated"), MockConstants.http200), tzktDelegatesURL: (MockConstants.jsonStub(fromFilename: "tzkt_ghostnet-bakers"), MockConstants.http200), // Media proxy diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json new file mode 100644 index 0000000..817613a --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_baker-config-tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4.json @@ -0,0 +1,21 @@ +{ + "address": "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4", + "name": "Teztillery", + "logo": "https://services.tzkt.io/v1/avatars/tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4", + "balance": 2187.553015, + "stakingBalance": 10120.963766, + "stakingCapacity": 11897.932014, + "maxStakingBalance": 11897.932014, + "freeSpace": 3098.9606940000003, + "fee": 0.0499, + "minDelegation": 10, + "payoutDelay": 1, + "payoutPeriod": 1, + "openForDelegation": true, + "estimatedRoi": 0.05916, + "serviceType": "tezos_only", + "serviceHealth": "active", + "payoutTiming": "no_data", + "payoutAccuracy": "no_data", + "insuranceCoverage": 0 + } \ No newline at end of file diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-none.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-none.json new file mode 100644 index 0000000..24e2455 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_delegator-rewards-none.json @@ -0,0 +1,106 @@ +[ + { + "cycle": 752, + "delegatedBalance": 730253, + "stakedBalance": 0, + "baker": { + "alias": "Teztillery", + "address": "tz1bdTgmF8pzBH9chtJptsjjrh5UfSXp1SQ4" + }, + "bakingPower": 0, + "totalBakingPower": 378988343190364, + "bakerDelegatedBalance": 335560569, + "externalDelegatedBalance": 7535104289, + "bakerStakedBalance": 1321992446, + "externalStakedBalance": 0, + "expectedBlocks": 0, + "expectedEndorsements": 0, + "futureBlocks": 0, + "futureBlockRewards": 0, + "blocks": 0, + "blockRewardsDelegated": 0, + "blockRewardsStakedOwn": 0, + "blockRewardsStakedEdge": 0, + "blockRewardsStakedShared": 0, + "missedBlocks": 0, + "missedBlockRewards": 0, + "futureEndorsements": 0, + "futureEndorsementRewards": 0, + "endorsements": 0, + "endorsementRewardsDelegated": 0, + "endorsementRewardsStakedOwn": 0, + "endorsementRewardsStakedEdge": 0, + "endorsementRewardsStakedShared": 0, + "missedEndorsements": 0, + "missedEndorsementRewards": 0, + "blockFees": 0, + "missedBlockFees": 0, + "doubleBakingRewards": 0, + "doubleBakingLostStaked": 0, + "doubleBakingLostUnstaked": 0, + "doubleBakingLostExternalStaked": 0, + "doubleBakingLostExternalUnstaked": 0, + "doubleEndorsingRewards": 0, + "doubleEndorsingLostStaked": 0, + "doubleEndorsingLostUnstaked": 0, + "doubleEndorsingLostExternalStaked": 0, + "doubleEndorsingLostExternalUnstaked": 0, + "doublePreendorsingRewards": 0, + "doublePreendorsingLostStaked": 0, + "doublePreendorsingLostUnstaked": 0, + "doublePreendorsingLostExternalStaked": 0, + "doublePreendorsingLostExternalUnstaked": 0, + "vdfRevelationRewardsDelegated": 0, + "vdfRevelationRewardsStakedOwn": 0, + "vdfRevelationRewardsStakedEdge": 0, + "vdfRevelationRewardsStakedShared": 0, + "nonceRevelationRewardsDelegated": 0, + "nonceRevelationRewardsStakedOwn": 0, + "nonceRevelationRewardsStakedEdge": 0, + "nonceRevelationRewardsStakedShared": 0, + "nonceRevelationLosses": 0, + "blockRewardsLiquid": 0, + "endorsementRewardsLiquid": 0, + "nonceRevelationRewardsLiquid": 0, + "vdfRevelationRewardsLiquid": 0, + "revelationRewards": 0, + "revelationLosses": 0, + "doublePreendorsingLosses": 0, + "doubleEndorsingLosses": 0, + "doubleBakingLosses": 0, + "endorsementRewards": 0, + "blockRewards": 0, + "stakingBalance": 9192657304, + "activeStake": 0, + "selectedStake": 378988343190364, + "balance": 730253, + "ownBlocks": 0, + "extraBlocks": 0, + "missedOwnBlocks": 0, + "missedExtraBlocks": 0, + "uncoveredOwnBlocks": 0, + "uncoveredExtraBlocks": 0, + "uncoveredEndorsements": 0, + "ownBlockRewards": 0, + "extraBlockRewards": 0, + "missedOwnBlockRewards": 0, + "missedExtraBlockRewards": 0, + "uncoveredOwnBlockRewards": 0, + "uncoveredExtraBlockRewards": 0, + "uncoveredEndorsementRewards": 0, + "ownBlockFees": 0, + "extraBlockFees": 0, + "missedOwnBlockFees": 0, + "missedExtraBlockFees": 0, + "uncoveredOwnBlockFees": 0, + "uncoveredExtraBlockFees": 0, + "doubleBakingLostDeposits": 0, + "doubleBakingLostRewards": 0, + "doubleBakingLostFees": 0, + "doubleEndorsingLostDeposits": 0, + "doubleEndorsingLostRewards": 0, + "doubleEndorsingLostFees": 0, + "revelationLostRewards": 0, + "revelationLostFees": 0 + } + ] \ No newline at end of file diff --git a/Tests/KukaiCoreSwiftTests/Stubs/tzkt_suggest-teztillery.json b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_suggest-teztillery.json new file mode 100644 index 0000000..d0fce60 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/tzkt_suggest-teztillery.json @@ -0,0 +1,6 @@ +[ + { + "alias": "Tezex Bakery Payouts", + "address": "tz1cBvEkYWV7RjTkd2kEcMPLwP62pqivXNEi" + } + ] \ No newline at end of file From c388453654994c66dce670849ca58abd1d72d5ee Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 11 Jul 2024 09:56:47 +0100 Subject: [PATCH 07/42] add validation to TzKT fetch methods --- .../KukaiCoreSwift/Clients/TzKTClient.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift index 431512f..a1e88ef 100644 --- a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift @@ -18,6 +18,7 @@ public class TzKTClient { /// Unique Errors that TzKTClient can throw public enum TzKTServiceError: Error { case invalidURL + case invalidAddress case parseError(String) } @@ -714,6 +715,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing the number or an error */ public func getBalanceCount(forAddress: String, completion: @escaping (Result) -> Void) { + guard forAddress != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + var url = config.tzktURL url.appendPathComponent("v1/tokens/balances/count") url.appendQueryItem(name: "account", value: forAddress) @@ -731,6 +737,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing an array of balances or an error */ public func getBalancePage(forAddress: String, offset: Int = 0, completion: @escaping ((Result<[TzKTBalance], KukaiError>) -> Void)) { + guard forAddress != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + var url = config.tzktURL url.appendPathComponent("v1/tokens/balances") url.appendQueryItem(name: "account", value: forAddress) @@ -750,6 +761,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing an object or an error */ public func getAccount(forAddress: String, fromURL: URL? = nil, completion: @escaping ((Result) -> Void)) { + guard forAddress != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + var url = fromURL == nil ? config.tzktURL : fromURL url?.appendPathComponent("v1/accounts/\(forAddress)") @@ -769,6 +785,11 @@ public class TzKTClient { - parameter completion: The completion block called with a `Result` containing an object or an error */ public func getAllBalances(forAddress address: String, completion: @escaping ((Result) -> Void)) { + guard address != "" else { + completion(Result.failure(KukaiError.internalApplicationError(error: TzKTServiceError.invalidAddress))) + return + } + getBalanceCount(forAddress: address) { [weak self] result in guard let tokenCount = try? result.get() else { completion(Result.failure(result.getFailure())) @@ -959,6 +980,11 @@ public class TzKTClient { /// Fetch all transactions, both account operations, and token transfers, and combine them into 1 response public func fetchTransactions(forAddress address: String, limit: Int = 50, completion: @escaping (([TzKTTransaction]) -> Void)) { + guard address != "" else { + completion([]) + return + } + let dispatchGroupTransactions = DispatchGroup() dispatchGroupTransactions.enter() dispatchGroupTransactions.enter() From a975365768b92efc5e418596c01249cf9916abac Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 11 Jul 2024 11:58:16 +0100 Subject: [PATCH 08/42] prevent TzKT from listening to all wallets --- Sources/KukaiCoreSwift/Clients/TzKTClient.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift index a1e88ef..4498a0e 100644 --- a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift @@ -1148,6 +1148,7 @@ public class TzKTClient { extension TzKTClient: HubConnectionDelegate { public func connectionDidOpen(hubConnection: HubConnection) { + if addressesToWatch.count == 0 { return } // Request to be subscribed to events belonging to the given account let subscription = AccountSubscription(addresses: addressesToWatch) From 2935cd7930f1ca7a32bdacbfb26ef832d56d1f91 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 16 Jul 2024 14:33:23 +0100 Subject: [PATCH 09/42] - update error handling to display nicer messages for some explicit cases - add extra test to catch another case - update existing strings --- .../Services/ErrorHandlingService.swift | 63 +++- .../Clients/TezosNodeClientTests.swift | 2 +- .../Services/ErrorHandlingServiceTests.swift | 33 +- .../Services/NetworkServiceTests.swift | 2 +- .../rpc_error_fa2_insufficient_balance.json | 330 ++++++++++++++++++ 5 files changed, 414 insertions(+), 16 deletions(-) create mode 100644 Tests/KukaiCoreSwiftTests/Stubs/rpc_error_fa2_insufficient_balance.json diff --git a/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift b/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift index a8b4282..f7e3acd 100644 --- a/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift +++ b/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift @@ -128,7 +128,7 @@ public struct KukaiError: CustomStringConvertible, Error { switch errorType { case .rpc: if let rpcErrorString = rpcErrorString { - return "RPC: \(rpcErrorString.removeLeadingProtocolFromRPCError() ?? rpcErrorString)" + return rpcErrorString } return "RPC: Unknown" @@ -238,9 +238,13 @@ public class ErrorHandlingService { /// Convert an `OperationResponseInternalResultError` into a `KukaiError` and optionally log it to the central logger public static func fromOperationError(_ opError: OperationResponseInternalResultError, requestURL: URL?, andLog: Bool = true) -> KukaiError { let errorWithoutProtocol = opError.id.removeLeadingProtocolFromRPCError() - var errorToReturn = KukaiError(errorType: .rpc, knownErrorMessage: nil, subType: nil, rpcErrorString: errorWithoutProtocol, failWith: nil, requestURL: nil, requestJSON: nil, responseJSON: nil, httpStatusCode: nil) + var errorToReturn = KukaiError(errorType: .rpc, knownErrorMessage: nil, subType: nil, rpcErrorString: "RPC error code: \(errorWithoutProtocol ?? "Unknown")", failWith: nil, requestURL: nil, requestJSON: nil, responseJSON: nil, httpStatusCode: nil) - if (errorWithoutProtocol == "michelson_v1.runtime_error" || errorWithoutProtocol == "michelson_v1.script_rejected"), let withError = opError.with { + + if let result = knownRPCErrorString(rpcStringWithoutLeadingProtocol: errorWithoutProtocol, with: opError.with) { + errorToReturn = KukaiError.rpcError(rpcErrorString: result, andFailWith: opError.with, requestURL: requestURL) + + } else if (errorWithoutProtocol == "michelson_v1.runtime_error" || errorWithoutProtocol == "michelson_v1.script_rejected"), let withError = opError.with { if let failwith = withError.int, let failwithInt = Int(failwith) { // Smart contract failwith reached with an Int denoting an error code @@ -265,6 +269,56 @@ public class ErrorHandlingService { return errorToReturn } + public static func knownRPCErrorString(rpcStringWithoutLeadingProtocol: String?, with: FailWith?) -> String? { + guard let rpcStringWithoutLeadingProtocol = rpcStringWithoutLeadingProtocol else { return nil } + + switch rpcStringWithoutLeadingProtocol { + + // top level protocol errors + case "implicit.empty_implicit_contract": + return "Your account has a 0 XTZ balance and is unable to pay for the fees to complete the transaction." + + case "tez.subtraction_underflow": + return "Your account does not have enough XTZ to complete the transaction." + + + // known michelson contract related errors (e.g. insufficent balance for a known token standard) + // can appear in 2 places + case "michelson_v1.script_rejected": + if let string = with?.string { + return compareInnerString(string) + + } else if let string = with?.args?.first?["string"] { + return compareInnerString(string) + + } else { + return nil + } + + default: + return nil + } + } + + private static func compareInnerString(_ remoteString: String?) -> String? { + switch remoteString?.lowercased() { + case "fa2_insufficient_balance": + return "Insufficient NFT/DeFi token balance to complete the transaction" + + case "fa1.2_insufficientbalance": + return "Insufficient DeFi token balance to complete the transaction" + + case "notenoughbalance": + return "Insufficient token balance to complete the transaction" + + case .none: + return nil + + case .some(_): + return nil + } + } + /// Search an `OperationResponse` to see does it contain any errors, if so return the last one as a `KukaiError` and optionally log it to the central logger public static func searchOperationResponseForErrors(_ opResponse: OperationResponse, requestURL: URL?, andLog: Bool = true) -> KukaiError? { if let lastError = opResponse.errors().last { @@ -311,7 +365,8 @@ public class ErrorHandlingService { let lastError = errorArray.last, let errorString = lastError.id.removeLeadingProtocolFromRPCError() { - errorToReturn = KukaiError.rpcError(rpcErrorString: errorString, andFailWith: lastError.with, requestURL: requestURL) + let updatedString = knownRPCErrorString(rpcStringWithoutLeadingProtocol: errorString, with: lastError.with) + errorToReturn = KukaiError.rpcError(rpcErrorString: updatedString ?? "RPC error code: \(errorString)", andFailWith: lastError.with, requestURL: requestURL) } else { errorToReturn.addNetworkData(requestURL: requestURL, requestJSON: requestData, responseJSON: data, httpStatusCode: httpResponse.statusCode) } diff --git a/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift b/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift index 29ee94c..f09bc2c 100644 --- a/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift +++ b/Tests/KukaiCoreSwiftTests/Clients/TezosNodeClientTests.swift @@ -101,7 +101,7 @@ class TezosNodeClientTests: XCTestCase { XCTFail("Should have failed, got opHash instead") case .failure(let error): - XCTAssert(error.description == "RPC: contract.counter_in_the_future", error.description) + XCTAssert(error.description == "RPC error code: contract.counter_in_the_future", error.description) } expectation.fulfill() diff --git a/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift index 83ccac2..88cd623 100644 --- a/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift @@ -18,11 +18,11 @@ class ErrorHandlingServiceTests: XCTestCase { func testStaticConstrutors() { let error1 = KukaiError.rpcError(rpcErrorString: "testing RPC string", andFailWith: nil, requestURL: nil) XCTAssert(error1.rpcErrorString == "testing RPC string", error1.rpcErrorString ?? "-") - XCTAssert(error1.description == "RPC: testing RPC string", error1.description) + XCTAssert(error1.description == "testing RPC string", error1.description) let error2 = KukaiError.rpcError(rpcErrorString: "testing RPC string", andFailWith: FailWith(string: nil, int: "1", args: nil), requestURL: nil) XCTAssert(error2.rpcErrorString == "testing RPC string", error2.rpcErrorString ?? "-") - XCTAssert(error2.description == "RPC: testing RPC string", error2.description) + XCTAssert(error2.description == "testing RPC string", error2.description) let error3 = KukaiError.unknown(withString: "test unknown string") XCTAssert(error3.rpcErrorString == "test unknown string", error3.rpcErrorString ?? "-") @@ -105,8 +105,8 @@ class ErrorHandlingServiceTests: XCTestCase { let containsErrors1 = ErrorHandlingService.searchOperationResponseForErrors(ops, requestURL: nil) XCTAssert(containsErrors1?.errorType == .rpc) - XCTAssert(containsErrors1?.rpcErrorString == "gas_exhausted.operation", containsErrors1?.rpcErrorString ?? "-") - XCTAssert(containsErrors1?.description == "RPC: gas_exhausted.operation", containsErrors1?.description ?? "-") + XCTAssert(containsErrors1?.rpcErrorString == "RPC error code: gas_exhausted.operation", containsErrors1?.rpcErrorString ?? "-") + XCTAssert(containsErrors1?.description == "RPC error code: gas_exhausted.operation", containsErrors1?.description ?? "-") } func testOperationResponseParserFailWith() { @@ -127,7 +127,7 @@ class ErrorHandlingServiceTests: XCTestCase { let containsErrors1 = ErrorHandlingService.searchOperationResponseForErrors(ops, requestURL: nil) XCTAssert(containsErrors1?.errorType == .rpc) XCTAssert(containsErrors1?.rpcErrorString == "A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.rpcErrorString ?? "-") - XCTAssert(containsErrors1?.description == "RPC: A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.description ?? "-") + XCTAssert(containsErrors1?.description == "A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.description ?? "-") } func testJsonResponse() { @@ -139,8 +139,8 @@ class ErrorHandlingServiceTests: XCTestCase { } let result2 = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) - XCTAssert(result2?.rpcErrorString == "gas_exhausted.operation", result2?.rpcErrorString ?? "-") - XCTAssert(result2?.description == "RPC: gas_exhausted.operation", result2?.description ?? "-") + XCTAssert(result2?.rpcErrorString == "RPC error code: gas_exhausted.operation", result2?.rpcErrorString ?? "-") + XCTAssert(result2?.description == "RPC error code: gas_exhausted.operation", result2?.description ?? "-") } @@ -154,7 +154,7 @@ class ErrorHandlingServiceTests: XCTestCase { let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) XCTAssert(result?.rpcErrorString == "A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.rpcErrorString ?? "-") - XCTAssert(result?.description == "RPC: A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.description ?? "-") + XCTAssert(result?.description == "A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.description ?? "-") } func testNotEnoughBalanceResponse() { @@ -166,8 +166,21 @@ class ErrorHandlingServiceTests: XCTestCase { } let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) - XCTAssert(result?.rpcErrorString == "A FAILWITH instruction was reached: {\"args\": [[\"string\": \"NotEnoughBalance\"]]}", result?.rpcErrorString ?? "-") - XCTAssert(result?.description == "RPC: A FAILWITH instruction was reached: {\"args\": [[\"string\": \"NotEnoughBalance\"]]}", result?.description ?? "-") + XCTAssert(result?.rpcErrorString == "Insufficient token balance to complete the transaction", result?.rpcErrorString ?? "-") + XCTAssert(result?.description == "Insufficient token balance to complete the transaction", result?.description ?? "-") + } + + func testInsufficientBalance() { + let errorData = MockConstants.jsonStub(fromFilename: "rpc_error_fa2_insufficient_balance") + + guard let opResponse = try? JSONDecoder().decode(OperationResponse.self, from: errorData) else { + XCTFail("Couldn't parse data as [OperationResponse]") + return + } + + let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil) + XCTAssert(result?.rpcErrorString == "Insufficient NFT/DeFi token balance to complete the transaction", result?.rpcErrorString ?? "-") + XCTAssert(result?.description == "Insufficient NFT/DeFi token balance to complete the transaction", result?.description ?? "-") } func testFailWithParsers() { diff --git a/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift index dfee01c..403f284 100644 --- a/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/NetworkServiceTests.swift @@ -168,7 +168,7 @@ class NetworkServiceTests: XCTestCase { case .failure(let error): XCTAssert(error.errorType == .rpc) - XCTAssert(error.description == "RPC: contract.counter_in_the_future", error.description) + XCTAssert(error.description == "RPC error code: contract.counter_in_the_future", error.description) XCTAssert(error.requestURL?.absoluteString == MockConstants.shared.config.nodeURLs[1].appendingPathComponent("chains/main/blocks/head/helpers/preapply/operations").absoluteString, error.requestURL?.absoluteString ?? "-") } diff --git a/Tests/KukaiCoreSwiftTests/Stubs/rpc_error_fa2_insufficient_balance.json b/Tests/KukaiCoreSwiftTests/Stubs/rpc_error_fa2_insufficient_balance.json new file mode 100644 index 0000000..ee823c7 --- /dev/null +++ b/Tests/KukaiCoreSwiftTests/Stubs/rpc_error_fa2_insufficient_balance.json @@ -0,0 +1,330 @@ +{ + "contents": [ + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856031", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "approve", + "value": { + "prim": "Pair", + "args": [ + { + "string": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc" + }, + { + "int": "0" + } + ] + } + }, + "metadata": { + "operation_result": { + "status": "backtracked", + "consumed_milligas": "3471406", + "storage_size": "215587", + "lazy_storage_diff": [ + { + "kind": "big_map", + "id": "31", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprtms8xryAx1EVZU2kyeTJhP8o6RBLBip81R7h7jS13dXJmREbhb", + "key": { + "bytes": "05070701000000066c65646765720a00000016000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "bytes": "050707000a0200000000" + } + } + ] + } + } + ] + } + } + }, + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856032", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "approve", + "value": { + "prim": "Pair", + "args": [ + { + "string": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc" + }, + { + "int": "7349" + } + ] + } + }, + "metadata": { + "operation_result": { + "status": "backtracked", + "consumed_milligas": "3480969", + "storage_size": "215619", + "lazy_storage_diff": [ + { + "kind": "big_map", + "id": "31", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprtms8xryAx1EVZU2kyeTJhP8o6RBLBip81R7h7jS13dXJmREbhb", + "key": { + "bytes": "05070701000000066c65646765720a00000016000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "bytes": "050707000a020000002007040a0000001601ece491d1ef313570a669e1c6d96af52d1ce0785c0000b572" + } + } + ] + } + } + ] + } + } + }, + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856033", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "1000000", + "destination": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc", + "parameters": { + "entrypoint": "investLiquidity", + "value": { + "int": "7349" + } + }, + "metadata": { + "operation_result": { + "status": "backtracked", + "balance_updates": [ + { + "kind": "contract", + "contract": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "change": "-1000000", + "origin": "block" + }, + { + "kind": "contract", + "contract": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc", + "change": "1000000", + "origin": "block" + } + ], + "consumed_milligas": "8318704", + "storage_size": "83635", + "lazy_storage_diff": [ + { + "kind": "big_map", + "id": "1498", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1497", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1496", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1495", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1494", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprvNa7fxN79ehSzqQiNv6YMNBFbPnt9wUWhvKJpD2KdcmicK4R3f", + "key": { + "bytes": "000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "prim": "Pair", + "args": [ + { + "int": "289061751656371524" + }, + { + "int": "100397669321122290545" + } + ] + } + } + ] + } + }, + { + "kind": "big_map", + "id": "1493", + "diff": { + "action": "update", + "updates": [ + { + "key_hash": "exprvNa7fxN79ehSzqQiNv6YMNBFbPnt9wUWhvKJpD2KdcmicK4R3f", + "key": { + "bytes": "000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + "value": { + "prim": "Pair", + "args": [ + { + "prim": "Pair", + "args": [ + [], + { + "int": "1487105" + } + ] + }, + { + "int": "0" + } + ] + } + } + ] + } + }, + { + "kind": "big_map", + "id": "1492", + "diff": { + "action": "update", + "updates": [] + } + }, + { + "kind": "big_map", + "id": "1491", + "diff": { + "action": "update", + "updates": [] + } + } + ] + }, + "internal_operation_results": [ + { + "kind": "transaction", + "source": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc", + "nonce": 0, + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "transfer", + "value": { + "prim": "Pair", + "args": [ + { + "bytes": "000038a2241d7ebdb18ab60ec0cfe71d7e47680d78d5" + }, + { + "prim": "Pair", + "args": [ + { + "bytes": "01ece491d1ef313570a669e1c6d96af52d1ce0785c00" + }, + { + "int": "7348" + } + ] + } + ] + } + }, + "result": { + "status": "failed", + "errors": [ + { + "kind": "temporary", + "id": "proto.013-PtJakart.michelson_v1.runtime_error", + "contract_handle": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "contract_code": "Deprecated" + }, + { + "kind": "temporary", + "id": "proto.013-PtJakart.michelson_v1.script_rejected", + "location": 519, + "with": { + "string": "fa2_insufficient_balance" + } + } + ] + } + } + ] + } + }, + { + "kind": "transaction", + "source": "tz1QoUmcycUDaFGvuju2bmTSaCqQCMEpRcgs", + "fee": "0", + "counter": "6856034", + "gas_limit": "1040000", + "storage_limit": "60000", + "amount": "0", + "destination": "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn", + "parameters": { + "entrypoint": "approve", + "value": { + "prim": "Pair", + "args": [ + { + "string": "KT1WBLrLE2vG8SedBqiSJFm4VVAZZBytJYHc" + }, + { + "int": "0" + } + ] + } + }, + "metadata": { + "operation_result": { + "status": "skipped" + } + } + } + ] +} From ce9be84d54254ac954b36b4258481d83af0de496 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 30 Jul 2024 14:37:54 +0100 Subject: [PATCH 10/42] - add firstLevel and lastLevel info to tokens and NFTs from tzkt to allow ordering with custom grouping logic - update tests --- .../KukaiCoreSwift/Clients/TzKTClient.swift | 2 +- .../Models/BakingBad/TzKTBalance.swift | 6 ++++ Sources/KukaiCoreSwift/Models/NFT.swift | 8 +++++ Sources/KukaiCoreSwift/Models/Token.swift | 32 +++++++++++++------ Tests/KukaiCoreSwiftTests/MockConstants.swift | 2 +- .../Models/TokenTests.swift | 2 +- .../Models/TzKTTransactionTests.swift | 2 +- 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift index 4498a0e..48920fe 100644 --- a/Sources/KukaiCoreSwift/Clients/TzKTClient.swift +++ b/Sources/KukaiCoreSwift/Clients/TzKTClient.swift @@ -923,7 +923,7 @@ public class TzKTClient { continue } else if balance.token.metadata != nil { // Else create a Token object and put into array, if we have valid metadata (e.g. able to tell how many decimals it has) - tokens.append(Token(from: balance.token, andTokenAmount: balance.tokenAmount)) + tokens.append(Token(from: balance, andTokenAmount: balance.tokenAmount)) } } diff --git a/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift b/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift index be66298..aba0984 100644 --- a/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift +++ b/Sources/KukaiCoreSwift/Models/BakingBad/TzKTBalance.swift @@ -21,6 +21,12 @@ public struct TzKTBalance: Codable { /// Details about the Token public let token: TzKTBalanceToken + /// The block level where the token was first seen + public let firstLevel: Decimal + + /// The block level where the token was last seen + public let lastLevel: Decimal + /// Helper to convert the RPC token balance to a `TokenAmount` object public var tokenAmount: TokenAmount { return TokenAmount(fromRpcAmount: balance, decimalPlaces: token.metadata?.decimalsInt ?? 0) ?? .zero() diff --git a/Sources/KukaiCoreSwift/Models/NFT.swift b/Sources/KukaiCoreSwift/Models/NFT.swift index 955eb23..bdcebee 100644 --- a/Sources/KukaiCoreSwift/Models/NFT.swift +++ b/Sources/KukaiCoreSwift/Models/NFT.swift @@ -64,6 +64,12 @@ public struct NFT: Codable, Hashable { return favouriteSortIndex != nil } + /// The block level where the token was first seen + public var firstlevel: Decimal + + /// The block level where the token was last seen + public var lastLevel: Decimal + /** Create a more developer friednly `NFT` from a generic `TzKTBalance` object @@ -83,6 +89,8 @@ public struct NFT: Codable, Hashable { displayURI = URL(string: tzkt.token.metadata?.displayUri ?? "") thumbnailURI = URL(string: tzkt.token.metadata?.thumbnailUri ?? "") metadata = tzkt.token.metadata + firstlevel = tzkt.firstLevel + lastLevel = tzkt.lastLevel } /// Confomring to Equatable diff --git a/Sources/KukaiCoreSwift/Models/Token.swift b/Sources/KukaiCoreSwift/Models/Token.swift index 3eee7a5..b529d3c 100644 --- a/Sources/KukaiCoreSwift/Models/Token.swift +++ b/Sources/KukaiCoreSwift/Models/Token.swift @@ -87,6 +87,12 @@ public class Token: Codable, CustomStringConvertible { /// Recording if the position the index the user chose for the favourite token to appear public var favouriteSortIndex: Int? = nil + /// The block level where the token was first seen + public var firstlevel: Decimal + + /// The block level where the token was last seen + public var lastLevel: Decimal + /// The individual NFT's owned of this token type public var nfts: [NFT]? @@ -126,6 +132,8 @@ public class Token: Codable, CustomStringConvertible { self.tokenId = tokenId self.nfts = nfts self.mintingTool = mintingTool + self.firstlevel = 0 + self.lastLevel = 0 // TODO: make failable init if let faVersion = faVersion, faVersion == .fa2 && tokenId == nil { @@ -136,23 +144,25 @@ public class Token: Codable, CustomStringConvertible { /** Init a `Token` from an object returned by the TzKT API */ - public init(from: TzKTBalanceToken, andTokenAmount: TokenAmount, stakedTokenAmount: TokenAmount? = nil, unstakedTokenAmount: TokenAmount? = nil) { - let decimalsString = from.metadata?.decimals ?? "0" + public init(from: TzKTBalance, andTokenAmount: TokenAmount, stakedTokenAmount: TokenAmount? = nil, unstakedTokenAmount: TokenAmount? = nil) { + let decimalsString = from.token.metadata?.decimals ?? "0" let decimalsInt = Int(decimalsString) ?? 0 - let isNFT = (from.metadata?.artifactUri != nil && decimalsInt == 0 && from.standard == .fa2) + let isNFT = (from.token.metadata?.artifactUri != nil && decimalsInt == 0 && from.token.standard == .fa2) - self.name = from.contract.alias - self.symbol = isNFT ? from.contract.alias ?? "" : from.displaySymbol + self.name = from.token.contract.alias + self.symbol = isNFT ? from.token.contract.alias ?? "" : from.token.displaySymbol self.tokenType = isNFT ? .nonfungible : .fungible - self.faVersion = from.standard + self.faVersion = from.token.standard self.balance = andTokenAmount self.stakedBalance = stakedTokenAmount ?? .zeroBalance(decimalPlaces: andTokenAmount.decimalPlaces) self.unstakedBalance = unstakedTokenAmount ?? .zeroBalance(decimalPlaces: andTokenAmount.decimalPlaces) - self.thumbnailURL = from.metadata?.thumbnailURL ?? TzKTClient.avatarURL(forToken: from.contract.address) - self.tokenContractAddress = from.contract.address - self.tokenId = Decimal(string: from.tokenId) ?? 0 + self.thumbnailURL = from.token.metadata?.thumbnailURL ?? TzKTClient.avatarURL(forToken: from.token.contract.address) + self.tokenContractAddress = from.token.contract.address + self.tokenId = Decimal(string: from.token.tokenId) ?? 0 self.nfts = [] - self.mintingTool = from.metadata?.mintingTool + self.mintingTool = from.token.metadata?.mintingTool + self.firstlevel = from.firstLevel + self.lastLevel = from.lastLevel // TODO: make failable init if let faVersion = faVersion, faVersion == .fa2 && tokenId == nil { @@ -183,6 +193,8 @@ public class Token: Codable, CustomStringConvertible { self.tokenId = Decimal(string: from.token.tokenId) ?? 0 self.nfts = [] self.mintingTool = from.mintingTool + self.firstlevel = from.level + self.lastLevel = from.level // TODO: make failable init if let faVersion = faVersion, faVersion == .fa2 && tokenId == nil { diff --git a/Tests/KukaiCoreSwiftTests/MockConstants.swift b/Tests/KukaiCoreSwiftTests/MockConstants.swift index c550eca..cffef78 100644 --- a/Tests/KukaiCoreSwiftTests/MockConstants.swift +++ b/Tests/KukaiCoreSwiftTests/MockConstants.swift @@ -413,7 +413,7 @@ public struct MockConstants { public static let token10Decimals = Token(name: "Token 10 decimals", symbol: "TK10", tokenType: .fungible, faVersion: .fa2, balance: TokenAmount.zero(), thumbnailURL: nil, tokenContractAddress: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtar8nn9", tokenId: 0, nfts: nil, mintingTool: nil) public static let nftMetadata = TzKTBalanceMetadata(name: "NFT Name", symbol: "NFT", decimals: "0", formats: nil, displayUri: MediaProxySerivceTests.ipfsURIWithoutExtension, artifactUri: nil, thumbnailUri: MediaProxySerivceTests.ipfsURIWithExtension, description: "A sample description", mintingTool: nil, tags: nil, minter: nil, shouldPreferSymbol: nil, attributes: nil, ttl: nil) public static let tokenWithNFTs = Token(name: "Token with NFTS", symbol: "T-NFT", tokenType: .nonfungible, faVersion: .fa2, balance: TokenAmount.zero(), thumbnailURL: nil, tokenContractAddress: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtabc123", tokenId: 0, nfts: [ - NFT(fromTzKTBalance: TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: nil, address: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtabc123"), tokenId: "4", standard: .fa2, totalSupply: "1", metadata: MockConstants.nftMetadata))) + NFT(fromTzKTBalance: TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: nil, address: "KT1G1cCRNBgQ48mVDjopHjEmTN5Sbtabc123"), tokenId: "4", standard: .fa2, totalSupply: "1", metadata: MockConstants.nftMetadata), firstLevel: 0, lastLevel: 0)) ], mintingTool: nil) diff --git a/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift b/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift index 4cd52dd..3d03cf5 100644 --- a/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/TokenTests.swift @@ -21,7 +21,7 @@ class TokenTests: XCTestCase { func testToken() { let token = Token(name: "test1", symbol: "T", tokenType: .fungible, faVersion: .fa1_2, balance: TokenAmount(fromNormalisedAmount: 3, decimalPlaces: 4), thumbnailURL: URL(string: "ipfs://abcdefgh1234"), tokenContractAddress: "KT1abc", tokenId: nil, nfts: nil, mintingTool: nil) - let tzktBalance = TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: "Test Alias", address: "KT1abc"), tokenId: "0", standard: .fa2, totalSupply: "1", metadata: nil)) + let tzktBalance = TzKTBalance(balance: "1", token: TzKTBalanceToken(contract: TzKTAddress(alias: "Test Alias", address: "KT1abc"), tokenId: "0", standard: .fa2, totalSupply: "1", metadata: nil), firstLevel: 0, lastLevel: 0) let nft = NFT(fromTzKTBalance: tzktBalance) let token2 = Token(name: "test2", symbol: "F", tokenType: .nonfungible, faVersion: .fa2, balance: TokenAmount.zero(), thumbnailURL: URL(string: "ipfs://abcdefgh1234"), tokenContractAddress: "KT1abc", tokenId: 0, nfts: [nft], mintingTool: nil) diff --git a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift index 0be2ccf..3c8ec05 100644 --- a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift @@ -97,7 +97,7 @@ final class TzKTTransactionTests: XCTestCase { transaction.processAdditionalData(withCurrentWalletAddress: "tz1abc") if let json = try? JSONEncoder().encode(transaction), let jsonString = String(data: json, encoding: .utf8) { - XCTAssert(jsonString.count == 944, jsonString.count.description) + XCTAssert(jsonString.count == 973, jsonString.count.description) } else { XCTFail() } From a749cc56f841b1b59cd9e86f685360246ec2b293 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Mon, 12 Aug 2024 15:56:28 +0100 Subject: [PATCH 11/42] add sync wrapper around ledger getAddress function so it can be more easily integrated into existing code --- .../Services/LedgerService.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index 03b6fca..21beb92 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -143,6 +143,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele private var bag_connection = Set() private var bag_writer = Set() private var bag_apdu = Set() + private var bag_addressFetcher = Set() private var counter = 0 /// Public shared instace to avoid having multiple copies of the underlying `JSContext` created @@ -329,6 +330,29 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele }).eraseToAnyPublisher() } + /** + Get a TZ address and public key from the current connected Ledger device + - parameter forDerivationPath: Optional. The derivation path to use to extract the address from the underlying HD wallet + - parameter curve: Optional. The `EllipticalCurve` to use to extract the address + - parameter verify: Whether or not to ask the ledger device to prompt the user to show them what the TZ address should be, to ensure the mobile matches + - returns: An async `Result` object, allowing code to be triggered via while loops more easily + */ + public func getAddress(forDerivationPath derivationPath: String = HD.defaultDerivationPath, curve: EllipticalCurve = .ed25519, verify: Bool) async -> Result<(address: String, publicKey: String), KukaiError> { + return await withCheckedContinuation({ continuation in + var cancellable: AnyCancellable! + cancellable = getAddress(forDerivationPath: derivationPath, curve: curve, verify: verify) + .sink(onError: { error in + continuation.resume(returning: Result.failure(error)) + }, onSuccess: { addressObj in + continuation.resume(returning: Result.success(addressObj)) + }, onComplete: { [weak self] in + self?.bag_addressFetcher.remove(cancellable) + }) + + cancellable.store(in: &bag_addressFetcher) + }) + } + /** Sign an operation payload with the underlying secret key, returning the signature - parameter hex: An operation converted to JSON, forged and watermarked, converted to a hex string. (Note: there are some issues with the ledger app signing batch transactions. May simply return no result at all. Can't run REVEAL and TRANSACTION together for example) From 2416714e4de7917834e8b068cd99d4150c25767d Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 13 Aug 2024 15:59:08 +0100 Subject: [PATCH 12/42] - convert walletmetadata and list to classes so we can use pass by reference - update caching/naming/deleting logic to use references to arrays in a generic way so that we can share bits of similar logic - add some ledger specific helpers - add new tests --- .../Models/WalletMetadata.swift | 43 +++++-- .../Services/WalletCacheService.swift | 107 ++++++++++++------ .../Models/TzKTTransactionTests.swift | 2 +- .../Services/WalletCacheServiceTests.swift | 50 +++++--- 4 files changed, 146 insertions(+), 56 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift index 2f80775..2b4a816 100644 --- a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift +++ b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift @@ -8,7 +8,7 @@ import Foundation /// Container to store groups of WalletMetadata based on type -public struct WalletMetadataList: Codable, Hashable { +public class WalletMetadataList: Codable, Hashable { public var socialWallets: [WalletMetadata] public var hdWallets: [WalletMetadata] public var linearWallets: [WalletMetadata] @@ -85,7 +85,7 @@ public struct WalletMetadataList: Codable, Hashable { return nil } - public mutating func update(address: String, with newMetadata: WalletMetadata) -> Bool { + public func update(address: String, with newMetadata: WalletMetadata) -> Bool { for (index, metadata) in socialWallets.enumerated() { if metadata.address == address { socialWallets[index] = newMetadata; return true } } @@ -113,8 +113,8 @@ public struct WalletMetadataList: Codable, Hashable { return false } - public mutating func set(mainnetDomain: TezosDomainsReverseRecord?, ghostnetDomain: TezosDomainsReverseRecord?, forAddress address: String) -> Bool { - var meta = metadata(forAddress: address) + public func set(mainnetDomain: TezosDomainsReverseRecord?, ghostnetDomain: TezosDomainsReverseRecord?, forAddress address: String) -> Bool { + let meta = metadata(forAddress: address) if let mainnet = mainnetDomain { meta?.mainnetDomains = [mainnet] @@ -131,8 +131,8 @@ public struct WalletMetadataList: Codable, Hashable { return false } - public mutating func set(nickname: String?, forAddress address: String) -> Bool { - var meta = metadata(forAddress: address) + public func set(nickname: String?, forAddress address: String) -> Bool { + let meta = metadata(forAddress: address) meta?.walletNickname = nickname if let meta = meta, update(address: address, with: meta) { @@ -142,8 +142,8 @@ public struct WalletMetadataList: Codable, Hashable { return false } - public mutating func set(hdWalletGroupName: String, forAddress address: String) -> Bool { - var meta = metadata(forAddress: address) + public func set(hdWalletGroupName: String, forAddress address: String) -> Bool { + let meta = metadata(forAddress: address) meta?.hdWalletGroupName = hdWalletGroupName if let meta = meta, update(address: address, with: meta) { @@ -224,6 +224,22 @@ public struct WalletMetadataList: Codable, Hashable { return temp } + + public static func == (lhs: WalletMetadataList, rhs: WalletMetadataList) -> Bool { + return lhs.socialWallets == rhs.socialWallets && + lhs.hdWallets == rhs.hdWallets && + lhs.linearWallets == rhs.linearWallets && + lhs.ledgerWallets == rhs.ledgerWallets && + lhs.watchWallets == rhs.watchWallets + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(socialWallets) + hasher.combine(hdWallets) + hasher.combine(linearWallets) + hasher.combine(ledgerWallets) + hasher.combine(watchWallets) + } } @@ -231,7 +247,7 @@ public struct WalletMetadataList: Codable, Hashable { /// Object to store UI related info about wallets, seperated from the wallet object itself to avoid issues merging together -public struct WalletMetadata: Codable, Hashable { +public class WalletMetadata: Codable, Hashable { public var address: String public var hdWalletGroupName: String? public var walletNickname: String? @@ -246,6 +262,7 @@ public struct WalletMetadata: Codable, Hashable { public var isWatchOnly: Bool public var bas58EncodedPublicKey: String public var backedUp: Bool + public var customDerivationPath: String? public func hasMainnetDomain() -> Bool { return (mainnetDomains ?? []).count > 0 @@ -287,7 +304,12 @@ public struct WalletMetadata: Codable, Hashable { } } - public init(address: String, hdWalletGroupName: String?, walletNickname: String? = nil, socialUsername: String? = nil, socialUserId: String? = nil, mainnetDomains: [TezosDomainsReverseRecord]? = nil, ghostnetDomains: [TezosDomainsReverseRecord]? = nil, socialType: TorusAuthProvider? = nil, type: WalletType, children: [WalletMetadata], isChild: Bool, isWatchOnly: Bool, bas58EncodedPublicKey: String, backedUp: Bool) { + public func childCountExcludingCustomDerivationPaths() -> Int { + let excluded = children.filter { $0.customDerivationPath == nil } + return excluded.count + } + + public init(address: String, hdWalletGroupName: String?, walletNickname: String? = nil, socialUsername: String? = nil, socialUserId: String? = nil, mainnetDomains: [TezosDomainsReverseRecord]? = nil, ghostnetDomains: [TezosDomainsReverseRecord]? = nil, socialType: TorusAuthProvider? = nil, type: WalletType, children: [WalletMetadata], isChild: Bool, isWatchOnly: Bool, bas58EncodedPublicKey: String, backedUp: Bool, customDerivationPath: String?) { self.address = address self.hdWalletGroupName = hdWalletGroupName self.walletNickname = walletNickname @@ -302,6 +324,7 @@ public struct WalletMetadata: Codable, Hashable { self.isWatchOnly = isWatchOnly self.bas58EncodedPublicKey = bas58EncodedPublicKey self.backedUp = backedUp + self.customDerivationPath = customDerivationPath } public static func == (lhs: WalletMetadata, rhs: WalletMetadata) -> Bool { diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 97c6749..8b96002 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -85,7 +85,7 @@ public class WalletCacheService { - Parameter childOfIndex: An optional `Int` to denote the index of the HD wallet that this wallet is a child of - Returns: Bool, indicating if the storage was successful or not */ - public func cache(wallet: T, childOfIndex: Int?, backedUp: Bool) throws { + public func cache(wallet: T, childOfIndex: Int?, backedUp: Bool, customDerivationPath: String? = nil) throws { guard let existingWallets = readWalletsFromDiskAndDecrypt() else { Logger.walletCache.error("cache - Unable to cache wallet, as can't decrypt existing wallets") throw WalletCacheError.unableToDecrypt @@ -99,40 +99,57 @@ public class WalletCacheService { var newWallets = existingWallets newWallets[wallet.address] = wallet - var newMetadata = readMetadataFromDiskAndDecrypt() + let newMetadata = readMetadataFromDiskAndDecrypt() + var array = metadataArray(forType: wallet.type, fromMeta: newMetadata) + if let index = childOfIndex { - if index >= newMetadata.hdWallets.count { - Logger.walletCache.error("WalletCacheService metadata insertion issue. Requested to add to HDWallet at index \"\(index)\", when there are currently only \"\(newMetadata.hdWallets.count)\" items") + + // If child index is present, update the correct sub array to include this new item, checking forst that we have the correct details + if index >= array.count { + Logger.walletCache.error("WalletCacheService metadata insertion issue. Requested to add at index \"\(index)\", when there are currently only \"\(array.count)\" items") throw WalletCacheError.requestedIndexTooHigh } - newMetadata.hdWallets[index].children.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, type: wallet.type, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + array[index].children.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, type: wallet.type, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) - } else if let _ = wallet as? HDWallet { + } else if wallet.type == .hd || wallet.type == .ledger { + + // If its HD or Ledger (also a HD), these wallets display grouped together with a custom name. Compute the new default name based off existing data and then add + var groupNameStart = "" + switch wallet.type { + case .hd: + groupNameStart = "HD Wallet " + case .ledger: + groupNameStart = "Ledger Wallet " + case .social, .regular, .regularShifted: + groupNameStart = "" + } var newNumber = 0 - if let lastDefaultName = newMetadata.hdWallets.reversed().first(where: { $0.hdWalletGroupName?.prefix(10) == "HD Wallet " }) { - let numberOnly = lastDefaultName.hdWalletGroupName?.replacingOccurrences(of: "HD Wallet ", with: "") + if let lastDefaultName = array.reversed().first(where: { $0.hdWalletGroupName?.prefix(groupNameStart.count) ?? " " == groupNameStart }) { + let numberOnly = lastDefaultName.hdWalletGroupName?.replacingOccurrences(of: groupNameStart, with: "") newNumber = (Int(numberOnly ?? "0") ?? 0) + 1 } if newNumber == 0 { - newNumber = newMetadata.hdWallets.count + 1 + newNumber = array.count + 1 } - newMetadata.hdWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: "HD Wallet \(newNumber)", walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + array.append(WalletMetadata(address: wallet.address, hdWalletGroupName: "\(groupNameStart)\(newNumber)", walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) } else if let torusWallet = wallet as? TorusWallet { - newMetadata.socialWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: torusWallet.socialUsername, socialUserId: torusWallet.socialUserId, socialType: torusWallet.authProvider, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) - } else if let _ = wallet as? LedgerWallet { - newMetadata.ledgerWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + // If social, cast and fetch special attributes + array.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: torusWallet.socialUsername, socialUserId: torusWallet.socialUserId, socialType: torusWallet.authProvider, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) } else { - newMetadata.linearWallets.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) + + // Else, add basic wallet to the list its supposed to go to + array.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) } - + // Update wallet metadata array, and then commit to disk + updateMetadataArray(forType: wallet.type, withNewArray: array, frorMeta: newMetadata) if encryptAndWriteWalletsToDisk(wallets: newWallets) && encryptAndWriteMetadataToDisk(newMetadata) == false { throw WalletCacheError.unableToEncryptAndWrite } else { @@ -140,6 +157,38 @@ public class WalletCacheService { } } + /// Helper method to return the appropriate sub array for the type, to reduce code compelxity + private func metadataArray(forType: WalletType, fromMeta: WalletMetadataList) -> [WalletMetadata] { + switch forType { + case .regular: + return fromMeta.linearWallets + case .regularShifted: + return fromMeta.linearWallets + case .hd: + return fromMeta.hdWallets + case .social: + return fromMeta.socialWallets + case .ledger: + return fromMeta.ledgerWallets + } + } + + /// Helper method to take ina new sub array and update and existing reference, to reduce code complexity + private func updateMetadataArray(forType: WalletType, withNewArray: [WalletMetadata], frorMeta: WalletMetadataList) { + switch forType { + case .regular: + frorMeta.linearWallets = withNewArray + case .regularShifted: + frorMeta.linearWallets = withNewArray + case .hd: + frorMeta.hdWallets = withNewArray + case .social: + frorMeta.socialWallets = withNewArray + case .ledger: + frorMeta.ledgerWallets = withNewArray + } + } + private func removeNewAddressFromWatchListIfExists(_ address: String, list: WalletMetadataList) { if let _ = list.watchWallets.first(where: { $0.address == address }) { let _ = deleteWatchWallet(address: address) @@ -150,7 +199,7 @@ public class WalletCacheService { Cahce a watch wallet metadata obj, only. Metadata cahcing handled via wallet cache method */ public func cacheWatchWallet(metadata: WalletMetadata) throws { - var list = readMetadataFromDiskAndDecrypt() + let list = readMetadataFromDiskAndDecrypt() if let _ = list.addresses().first(where: { $0 == metadata.address }) { Logger.walletCache.error("cacheWatchWallet - Unable to cache wallet, wallet already exists") @@ -177,37 +226,31 @@ public class WalletCacheService { } var newWallets = existingWallets + let type = existingWallets[withAddress]?.type ?? .hd newWallets.removeValue(forKey: withAddress) - var newMetadata = readMetadataFromDiskAndDecrypt() + let newMetadata = readMetadataFromDiskAndDecrypt() + var array = metadataArray(forType: type, fromMeta: newMetadata) + if let hdWalletIndex = parentIndex { - guard hdWalletIndex < newMetadata.hdWallets.count, let childIndex = newMetadata.hdWallets[hdWalletIndex].children.firstIndex(where: { $0.address == withAddress }) else { + guard hdWalletIndex < array.count, let childIndex = array[hdWalletIndex].children.firstIndex(where: { $0.address == withAddress }) else { Logger.walletCache.error("Unable to locate wallet") return false } - let _ = newMetadata.hdWallets[hdWalletIndex].children.remove(at: childIndex) + let _ = array[hdWalletIndex].children.remove(at: childIndex) } else { - if let index = newMetadata.hdWallets.firstIndex(where: { $0.address == withAddress }) { + if let index = array.firstIndex(where: { $0.address == withAddress }) { // Children will be removed from metadata automatically, as they are contained inside the parent, however they won't from the encrypted cache // Remove them from encrypted first, then parent from metadata - let children = newMetadata.hdWallets[index].children + let children = array[index].children for child in children { newWallets.removeValue(forKey: child.address) } - let _ = newMetadata.hdWallets.remove(at: index) - - } else if let index = newMetadata.socialWallets.firstIndex(where: { $0.address == withAddress }) { - let _ = newMetadata.socialWallets.remove(at: index) - - } else if let index = newMetadata.linearWallets.firstIndex(where: { $0.address == withAddress }) { - let _ = newMetadata.linearWallets.remove(at: index) - - } else if let index = newMetadata.ledgerWallets.firstIndex(where: { $0.address == withAddress }) { - let _ = newMetadata.ledgerWallets.remove(at: index) + let _ = array.remove(at: index) } else { Logger.walletCache.error("Unable to locate wallet") @@ -222,7 +265,7 @@ public class WalletCacheService { Clear a watch wallet meatadata obj from the metadata cache only, does not affect actual wallet cache */ public func deleteWatchWallet(address: String) -> Bool { - var list = readMetadataFromDiskAndDecrypt() + let list = readMetadataFromDiskAndDecrypt() list.watchWallets.removeAll(where: { $0.address == address }) return encryptAndWriteMetadataToDisk(list) diff --git a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift index 3c8ec05..98e9b31 100644 --- a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift @@ -78,7 +78,7 @@ final class TzKTTransactionTests: XCTestCase { } func testPlaceholders() { - let source = WalletMetadata(address: "tz1abc", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let source = WalletMetadata(address: "tz1abc", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) 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, baker: nil, kind: nil) diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index 58ebf09..2147414 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -211,7 +211,6 @@ class WalletCacheServiceTests: XCTestCase { XCTFail("Should not error: \(error)") } - // Update one and check let _ = list.set(hdWalletGroupName: "Blah 2", forAddress: hdWallet2.address) let _ = walletCacheService.encryptAndWriteMetadataToDisk(list) @@ -250,7 +249,7 @@ class WalletCacheServiceTests: XCTestCase { func testMetadata() { let mainentDomain = [TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.tez", address: "tz1abc123"))] let ghostnetDomain = [TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.gho", address: "tz1abc123"))] - let metadata1 = WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: mainentDomain, ghostnetDomains: ghostnetDomain, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let metadata1 = WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: mainentDomain, ghostnetDomains: ghostnetDomain, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) XCTAssert(metadata1.hasMainnetDomain()) XCTAssert(metadata1.hasGhostnetDomain()) @@ -265,7 +264,7 @@ class WalletCacheServiceTests: XCTestCase { - let metadata2 = WalletMetadata(address: "tz1def456", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let metadata2 = WalletMetadata(address: "tz1def456", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) XCTAssert(!metadata2.hasMainnetDomain()) XCTAssert(!metadata2.hasGhostnetDomain()) @@ -274,23 +273,23 @@ class WalletCacheServiceTests: XCTestCase { func testMetadataList() { let mainentDomain = TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.tez", address: "tz1abc123")) let ghostnetDomain = TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.gho", address: "tz1abc123")) - let child = WalletMetadata(address: "tz1child", hdWalletGroupName: nil, type: .hd, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) - let updatedWatch = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "blah", backedUp: true) + let child = WalletMetadata(address: "tz1child", hdWalletGroupName: nil, type: .hd, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + let updatedWatch = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "blah", backedUp: true, customDerivationPath: nil) let hd: [WalletMetadata] = [ - WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [child], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [child], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) ] let social: [WalletMetadata] = [ - WalletMetadata(address: "tz1def", hdWalletGroupName: nil, socialUsername: "test@gmail.com", socialType: .google, type: .social, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1def", hdWalletGroupName: nil, socialUsername: "test@gmail.com", socialType: .google, type: .social, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) ] let linear: [WalletMetadata] = [ - WalletMetadata(address: "tz1ghi", hdWalletGroupName: nil, type: .regular, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1ghi", hdWalletGroupName: nil, type: .regular, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) ] let watch: [WalletMetadata] = [ - WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) ] - var list = WalletMetadataList(socialWallets: social, hdWallets: hd, linearWallets: linear, ledgerWallets: [], watchWallets: watch) + let list = WalletMetadataList(socialWallets: social, hdWallets: hd, linearWallets: linear, ledgerWallets: [], watchWallets: watch) let addresses = list.addresses() XCTAssert(addresses == ["tz1def", "tz1abc123", "tz1child", "tz1ghi", "tz1jkl"], addresses.description) @@ -334,7 +333,7 @@ class WalletCacheServiceTests: XCTestCase { func testWatchWallet() { XCTAssert(walletCacheService.deleteAllCacheAndKeys()) - let watchWallet = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + let watchWallet = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) @@ -355,7 +354,7 @@ class WalletCacheServiceTests: XCTestCase { XCTFail("Should not error: \(error)") } - let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) @@ -369,7 +368,7 @@ class WalletCacheServiceTests: XCTestCase { XCTAssert(walletCacheService.deleteAllCacheAndKeys()) // Add watch and confirm - let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) @@ -391,4 +390,29 @@ class WalletCacheServiceTests: XCTestCase { XCTFail("Should not error: \(error)") } } + + func testLedgerWalletCaching() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + let ledgerWallet = LedgerWallet(address: "tz1abc", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + let ledgerWalletChild1 = LedgerWallet(address: "tz1def", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah2")! + let ledgerWalletChild2 = LedgerWallet(address: "tz1ghi", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah3")! + let ledgerWalletChild3 = LedgerWallet(address: "tz1jkl", publicKey: "edpks1234", derivationPath: "m/44'/1729'/147'/62'", curve: .ed25519, ledgerUUID: "blah4")! + + do { + try walletCacheService.cache(wallet: ledgerWallet, childOfIndex: nil, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild1, childOfIndex: 0, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild2, childOfIndex: 0, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild3, childOfIndex: 0, backedUp: true, customDerivationPath: ledgerWalletChild3.derivationPath) + + let list = walletCacheService.readMetadataFromDiskAndDecrypt() + let ledgers = list.ledgerWallets + let excludedCount = ledgers[0].childCountExcludingCustomDerivationPaths() + XCTAssert(ledgers.count == 1, ledgers.count.description) + XCTAssert(ledgers[0].children.count == 3, ledgers[0].children.count.description) + XCTAssert(excludedCount == 2, excludedCount.description) + } catch let error { + XCTFail("Should not error: \(error)") + } + } } From d89988e5b6cda4895b4af021384695234acd6c4a Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 14 Aug 2024 15:32:07 +0100 Subject: [PATCH 13/42] - bug fix: include ledger children in helper methods --- .../KukaiCoreSwift/Models/WalletMetadata.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift index 2b4a816..0e1ccba 100644 --- a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift +++ b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift @@ -66,6 +66,10 @@ public class WalletMetadataList: Codable, Hashable { for metadata in ledgerWallets { if metadata.address == address { return metadata } + + for childMetadata in metadata.children { + if childMetadata.address == address { return childMetadata } + } } for metaData in watchWallets { @@ -104,6 +108,10 @@ public class WalletMetadataList: Codable, Hashable { for (index, metadata) in ledgerWallets.enumerated() { if metadata.address == address { ledgerWallets[index] = newMetadata; return true } + + for (childIndex, childMetadata) in metadata.children.enumerated() { + if childMetadata.address == address { hdWallets[index].children[childIndex] = newMetadata; return true } + } } for (index, metadata) in watchWallets.enumerated() { @@ -154,12 +162,16 @@ public class WalletMetadataList: Codable, Hashable { } public func count() -> Int { - var total = (socialWallets.count + linearWallets.count + ledgerWallets.count + watchWallets.count) + var total = (socialWallets.count + linearWallets.count + watchWallets.count) for wallet in hdWallets { total += (1 + wallet.children.count) } + for wallet in ledgerWallets { + total += (1 + wallet.children.count) + } + return total } @@ -184,6 +196,10 @@ public class WalletMetadataList: Codable, Hashable { for metadata in ledgerWallets { temp.append(metadata.address) + + for childMetadata in metadata.children { + temp.append(childMetadata.address) + } } for metadata in watchWallets { From 6512793f6e1efef761b39b85637c31a9a0f43269 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 20 Aug 2024 12:19:33 +0100 Subject: [PATCH 14/42] bug fix ledger --- Sources/KukaiCoreSwift/Models/WalletMetadata.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift index 0e1ccba..370439d 100644 --- a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift +++ b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift @@ -98,7 +98,7 @@ public class WalletMetadataList: Codable, Hashable { if metadata.address == address { hdWallets[index] = newMetadata; return true } for (childIndex, childMetadata) in metadata.children.enumerated() { - if childMetadata.address == address { hdWallets[index].children[childIndex] = newMetadata; return true } + if childMetadata.address == address { hdWallets[index].children[childIndex] = newMetadata; return true } } } @@ -110,7 +110,7 @@ public class WalletMetadataList: Codable, Hashable { if metadata.address == address { ledgerWallets[index] = newMetadata; return true } for (childIndex, childMetadata) in metadata.children.enumerated() { - if childMetadata.address == address { hdWallets[index].children[childIndex] = newMetadata; return true } + if childMetadata.address == address { ledgerWallets[index].children[childIndex] = newMetadata; return true } } } From 6bf4f574d4683bcb345c4ccc5dd37af1029e98a7 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 21 Aug 2024 11:18:43 +0100 Subject: [PATCH 15/42] add timeouts to ledger sign process --- Sources/KukaiCoreSwift/Models/LedgerWallet.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index 9e74f11..e8a1bd2 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -78,8 +78,14 @@ public class LedgerWallet: Wallet { let isWatermarkedOperation = (String(hex.prefix(2)) == "03") && hex.count != 32 LedgerService.shared.connectTo(uuid: ledgerUUID) - .flatMap { _ -> AnyPublisher in + .timeout(.seconds(10), scheduler: RunLoop.main, customError: { + return KukaiError.knownErrorMessage("Timed out waiting for device to connect. Check device/bluetooth is turned on and try again") + }) + .flatMap { _ -> Publishers.Timeout, RunLoop> in return LedgerService.shared.sign(hex: hex, parse: isWatermarkedOperation) + .timeout(.seconds(10), scheduler: RunLoop.main, customError: { + return KukaiError.knownErrorMessage("Timed out waiting for device to connect. Check device/bluetooth is turned on and try again") + }) } .sink(onError: { error in completion(Result.failure(error)) From 9fdde00348748fa5f98621c44cc858d96f8a5739 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 21 Aug 2024 12:07:20 +0100 Subject: [PATCH 16/42] - remove timeout from ledger sign as that will take time - change ledgerStringToSign logic temporarily while we test the new app --- Sources/KukaiCoreSwift/Models/LedgerWallet.swift | 5 +---- Sources/KukaiCoreSwift/Services/OperationService.swift | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index e8a1bd2..8df5569 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -81,11 +81,8 @@ public class LedgerWallet: Wallet { .timeout(.seconds(10), scheduler: RunLoop.main, customError: { return KukaiError.knownErrorMessage("Timed out waiting for device to connect. Check device/bluetooth is turned on and try again") }) - .flatMap { _ -> Publishers.Timeout, RunLoop> in + .flatMap { _ -> AnyPublisher in return LedgerService.shared.sign(hex: hex, parse: isWatermarkedOperation) - .timeout(.seconds(10), scheduler: RunLoop.main, customError: { - return KukaiError.knownErrorMessage("Timed out waiting for device to connect. Check device/bluetooth is turned on and try again") - }) } .sink(onError: { error in completion(Result.failure(error)) diff --git a/Sources/KukaiCoreSwift/Services/OperationService.swift b/Sources/KukaiCoreSwift/Services/OperationService.swift index 0f827b8..583da48 100644 --- a/Sources/KukaiCoreSwift/Services/OperationService.swift +++ b/Sources/KukaiCoreSwift/Services/OperationService.swift @@ -160,7 +160,7 @@ public class OperationService { ledgerCanParse = true } - return (ledgerCanParse ? watermarkedOp : blakeHashString) + return watermarkedOp //(ledgerCanParse ? watermarkedOp : blakeHashString) } /** From 0e460d3e72387329ae3bdafbbfc1a7f1e3d08d12 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 21 Aug 2024 12:11:43 +0100 Subject: [PATCH 17/42] remove timeout for now --- Sources/KukaiCoreSwift/Models/LedgerWallet.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index 8df5569..9e74f11 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -78,9 +78,6 @@ public class LedgerWallet: Wallet { let isWatermarkedOperation = (String(hex.prefix(2)) == "03") && hex.count != 32 LedgerService.shared.connectTo(uuid: ledgerUUID) - .timeout(.seconds(10), scheduler: RunLoop.main, customError: { - return KukaiError.knownErrorMessage("Timed out waiting for device to connect. Check device/bluetooth is turned on and try again") - }) .flatMap { _ -> AnyPublisher in return LedgerService.shared.sign(hex: hex, parse: isWatermarkedOperation) } From c5a5fb3761e0c1de4a917038de20eb2ec1eeaa8e Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 21 Aug 2024 12:26:35 +0100 Subject: [PATCH 18/42] ledger: test different way to trigger parsing --- .../KukaiCoreSwift/Models/LedgerWallet.swift | 4 ++-- .../Services/OperationService.swift | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index 9e74f11..ce5f23a 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -75,11 +75,11 @@ public class LedgerWallet: Wallet { Please be careful when asking the Ledger to parse (passing in an operation), Ledgers have very limited display ability. Keep it to a single operation, not invoking a smart contract */ public func sign(_ hex: String, isOperation: Bool, completion: @escaping ((Result<[UInt8], KukaiError>) -> Void)) { - let isWatermarkedOperation = (String(hex.prefix(2)) == "03") && hex.count != 32 + //let isWatermarkedOperation = (String(hex.prefix(2)) == "03") && hex.count != 32 LedgerService.shared.connectTo(uuid: ledgerUUID) .flatMap { _ -> AnyPublisher in - return LedgerService.shared.sign(hex: hex, parse: isWatermarkedOperation) + return LedgerService.shared.sign(hex: hex, parse: isOperation) } .sink(onError: { error in completion(Result.failure(error)) diff --git a/Sources/KukaiCoreSwift/Services/OperationService.swift b/Sources/KukaiCoreSwift/Services/OperationService.swift index 583da48..2b1b21d 100644 --- a/Sources/KukaiCoreSwift/Services/OperationService.swift +++ b/Sources/KukaiCoreSwift/Services/OperationService.swift @@ -124,14 +124,16 @@ public class OperationService { /// Internal function to group together operations for readability sake private func signPreapplyAndInject(wallet: Wallet, forgedHash: String, operationPayload: OperationPayload, operationMetadata: OperationMetadata, completion: @escaping ((Result) -> Void)) { var stringToSign = forgedHash + var isOperation = true if wallet.type == .ledger { stringToSign = ledgerStringToSign(forgedHash: forgedHash, operationPayload: operationPayload) + isOperation = operationPayloadCanBeParsedByLedger(operationPayload) } // Sign whatever string is required, and move on to preapply / inject - wallet.sign(stringToSign, isOperation: true) { [weak self] result in + wallet.sign(stringToSign, isOperation: isOperation) { [weak self] result in guard let signature = try? result.get() else { completion(Result.failure(result.getFailure())) return @@ -141,6 +143,19 @@ public class OperationService { } } + private func operationPayloadCanBeParsedByLedger(_ operationPayload: OperationPayload) -> Bool { + if operationPayload.contents.count == 1, let first = operationPayload.contents.first { + if first is OperationReveal || first is OperationDelegation { + return true + + } else if first is OperationTransaction, let transOp = first as? OperationTransaction, transOp.parameters == nil { + return true + } + } + + return false + } + /** Ledger can only parse operations under certain conditions. These conditions are not documented well. This function will attempt to determine whether the payload can be parsed or not, and returnt he appropriate string for the LedgerWallet sign function It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error From 4bbde8f46a9ecf7fa4b26f8637d0ac5d8ed17ff4 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 21 Aug 2024 15:01:20 +0100 Subject: [PATCH 19/42] ledger test, force to always parse --- Sources/KukaiCoreSwift/Services/OperationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/OperationService.swift b/Sources/KukaiCoreSwift/Services/OperationService.swift index 2b1b21d..dd81d4a 100644 --- a/Sources/KukaiCoreSwift/Services/OperationService.swift +++ b/Sources/KukaiCoreSwift/Services/OperationService.swift @@ -128,7 +128,7 @@ public class OperationService { if wallet.type == .ledger { stringToSign = ledgerStringToSign(forgedHash: forgedHash, operationPayload: operationPayload) - isOperation = operationPayloadCanBeParsedByLedger(operationPayload) + isOperation = true //operationPayloadCanBeParsedByLedger(operationPayload) } From c0741151aa3ef92667e1348869c540968821c864 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 21 Aug 2024 16:52:48 +0100 Subject: [PATCH 20/42] test changing mtu --- Sources/KukaiCoreSwift/Services/LedgerService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index 21beb92..54691c6 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -185,7 +185,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele // Convert the supplied data into an APDU. Returns a single string per ADPU, but broken up into chunks, seperated by spaces for each maximum sized data packet - guard let sendAPDU = self?.jsContext.evaluateScript("ledger_app_tezos.sendAPDU(\"\(apdu)\", 156)").toString() else { + guard let sendAPDU = self?.jsContext.evaluateScript("ledger_app_tezos.sendAPDU(\"\(apdu)\", 20)").toString() else { self?.deviceConnectedPublisher.send(false) return } From 06f65e1a28e2448bc9fbc0624a0bb1f1c8bb6127 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Wed, 21 Aug 2024 17:26:52 +0100 Subject: [PATCH 21/42] change error formatting --- Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift b/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift index f7e3acd..f7359ed 100644 --- a/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift +++ b/Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift @@ -151,7 +151,7 @@ public struct KukaiError: CustomStringConvertible, Error { case .internalApplication: if let subType = subType { - return "Internal Application Error: \(subType.localizedDescription)" + return "Internal Application Error: \(subType)" } return "Internal Application Error: Unknown" From afceb726c9fd0102e4c51ccecf34522c7118c80d Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 22 Aug 2024 11:33:57 +0100 Subject: [PATCH 22/42] WIP: ledger test removing ledger specific formatting code --- .../KukaiCoreSwift/Models/LedgerWallet.swift | 63 ++++++++++++++++++- Sources/KukaiCoreSwift/Models/Wallet.swift | 1 + .../Services/LedgerService.swift | 1 + .../Services/OperationService.swift | 47 +------------- .../Services/OperationServiceTests.swift | 2 + 5 files changed, 67 insertions(+), 47 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index ce5f23a..116f484 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -75,11 +75,26 @@ public class LedgerWallet: Wallet { Please be careful when asking the Ledger to parse (passing in an operation), Ledgers have very limited display ability. Keep it to a single operation, not invoking a smart contract */ public func sign(_ hex: String, isOperation: Bool, completion: @escaping ((Result<[UInt8], KukaiError>) -> Void)) { - //let isWatermarkedOperation = (String(hex.prefix(2)) == "03") && hex.count != 32 + guard let bytes = Sodium.shared.utils.hex2bin(hex) else { + completion(Result.failure(KukaiError.internalApplicationError(error: WalletError.signatureError))) + return + } + + var bytesToSign: [UInt8] = [] + if isOperation { + bytesToSign = bytes.addOperationWatermarkAndHash() ?? [] + } else { + bytesToSign = Sodium.shared.genericHash.hash(message: bytes, outputLength: 32) ?? [] + } + + guard let hexString = Sodium.shared.utils.bin2hex(bytesToSign) else { + completion(Result.failure(KukaiError.internalApplicationError(error: WalletError.signatureError))) + return + } LedgerService.shared.connectTo(uuid: ledgerUUID) .flatMap { _ -> AnyPublisher in - return LedgerService.shared.sign(hex: hex, parse: isOperation) + return LedgerService.shared.sign(hex: hexString, parse: true) } .sink(onError: { error in completion(Result.failure(error)) @@ -95,6 +110,50 @@ public class LedgerWallet: Wallet { .store(in: &bag) } + /* + /** + Ledger can only parse operations under certain conditions. These conditions are not documented well. This function will attempt to determine whether the payload can be parsed or not, and returnt he appropriate string for the LedgerWallet sign function + It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error + If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead + */ + public func ledgerStringToSign(forgedHash: String, operationPayload: OperationPayload) -> String { + let watermarkedOp = "03" + forgedHash + let watermarkedBytes = Sodium.shared.utils.hex2bin(watermarkedOp) ?? [] + let blakeHash = Sodium.shared.genericHash.hash(message: watermarkedBytes, outputLength: 32) + let blakeHashString = blakeHash?.toHexString() ?? "" + + // Ledger can only parse operations under certain conditions. These conditions are not documented well. + // It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error + // If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead + var ledgerCanParse = false + if operationPayload.contents.count == 1, let first = operationPayload.contents.first, (first is OperationReveal || first is OperationDelegation || first is OperationTransaction) { + ledgerCanParse = true + } + + return watermarkedOp //(ledgerCanParse ? watermarkedOp : blakeHashString) + } + */ + + + /* + private func operationPayloadCanBeParsedByLedger(_ operationPayload: OperationPayload) -> Bool { + if operationPayload.contents.count == 1, let first = operationPayload.contents.first { + if first is OperationReveal || first is OperationDelegation { + return true + + } else if first is OperationTransaction, let transOp = first as? OperationTransaction, transOp.parameters == nil { + return true + } + } + + return false + } + */ + + + + + /** Function to extract the curve used to create the public key */ diff --git a/Sources/KukaiCoreSwift/Models/Wallet.swift b/Sources/KukaiCoreSwift/Models/Wallet.swift index fc28ab8..a6732fb 100644 --- a/Sources/KukaiCoreSwift/Models/Wallet.swift +++ b/Sources/KukaiCoreSwift/Models/Wallet.swift @@ -45,6 +45,7 @@ public protocol Wallet: Codable { /** Sign a hex string with the wallets private key - parameter hex: A hex encoded string, representing a forged operation payload. + - parameter isOperation: A boolean to indicate whether its an operation or something else such as an expression to sign. So that the appropriate prefix can be added automatically - parameter completion: A completion block to run with the resulting signature, needs to be done async in order to support usecases such as signing with an external ledger. */ func sign(_ hex: String, isOperation: Bool, completion: @escaping ((Result<[UInt8], KukaiError>) -> Void)) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index 54691c6..fa83f5a 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -77,6 +77,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele case LICENSING = "6f42" case HALTED = "6faa" + case APP_CLOSED = "6e01" case DEVICE_LOCKED = "009000" case UNKNOWN = "99999999" case NO_WRITE_CHARACTERISTIC = "99999996" diff --git a/Sources/KukaiCoreSwift/Services/OperationService.swift b/Sources/KukaiCoreSwift/Services/OperationService.swift index dd81d4a..324338f 100644 --- a/Sources/KukaiCoreSwift/Services/OperationService.swift +++ b/Sources/KukaiCoreSwift/Services/OperationService.swift @@ -123,17 +123,9 @@ public class OperationService { /// Internal function to group together operations for readability sake private func signPreapplyAndInject(wallet: Wallet, forgedHash: String, operationPayload: OperationPayload, operationMetadata: OperationMetadata, completion: @escaping ((Result) -> Void)) { - var stringToSign = forgedHash - var isOperation = true - - if wallet.type == .ledger { - stringToSign = ledgerStringToSign(forgedHash: forgedHash, operationPayload: operationPayload) - isOperation = true //operationPayloadCanBeParsedByLedger(operationPayload) - } - - + // Sign whatever string is required, and move on to preapply / inject - wallet.sign(stringToSign, isOperation: isOperation) { [weak self] result in + wallet.sign(forgedHash, isOperation: true) { [weak self] result in guard let signature = try? result.get() else { completion(Result.failure(result.getFailure())) return @@ -143,41 +135,6 @@ public class OperationService { } } - private func operationPayloadCanBeParsedByLedger(_ operationPayload: OperationPayload) -> Bool { - if operationPayload.contents.count == 1, let first = operationPayload.contents.first { - if first is OperationReveal || first is OperationDelegation { - return true - - } else if first is OperationTransaction, let transOp = first as? OperationTransaction, transOp.parameters == nil { - return true - } - } - - return false - } - - /** - Ledger can only parse operations under certain conditions. These conditions are not documented well. This function will attempt to determine whether the payload can be parsed or not, and returnt he appropriate string for the LedgerWallet sign function - It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error - If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead - */ - public func ledgerStringToSign(forgedHash: String, operationPayload: OperationPayload) -> String { - let watermarkedOp = "03" + forgedHash - let watermarkedBytes = Sodium.shared.utils.hex2bin(watermarkedOp) ?? [] - let blakeHash = Sodium.shared.genericHash.hash(message: watermarkedBytes, outputLength: 32) - let blakeHashString = blakeHash?.toHexString() ?? "" - - // Ledger can only parse operations under certain conditions. These conditions are not documented well. - // It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error - // If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead - var ledgerCanParse = false - if operationPayload.contents.count == 1, let first = operationPayload.contents.first, (first is OperationReveal || first is OperationDelegation || first is OperationTransaction) { - ledgerCanParse = true - } - - return watermarkedOp //(ledgerCanParse ? watermarkedOp : blakeHashString) - } - /** Preapply and Inject wrapped up as one function, for situations like Ledger Wallets, where signing is a complately different process, and must be done elsewhere - parameter forgedOperation: The forged operation hex without a watermark. diff --git a/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift index 26f23b1..5a2c6f4 100644 --- a/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift @@ -54,6 +54,7 @@ class OperationServiceTests: XCTestCase { wait(for: [expectation], timeout: 120) } + /* func testLedgerWithoutReveal() { let stringToSign = operationService.ledgerStringToSign( forgedHash: "43f597d84037e88354ed041cc6356f737cc6638691979bb64415451b58b4af2c6c00ad00bb6cbcfc497bffbaf54c23511c74dbeafb2d00ffde080000c0843d00005134b25890279835eb946e6369a3d719bc0d617700", @@ -73,4 +74,5 @@ class OperationServiceTests: XCTestCase { // Should be a 32 character long blake2b hash XCTAssert(stringToSign == "a49d693a7bf5c22564bbb9368c94362ea64f07e5c5d1c443b63190ae5c85adf2", stringToSign) } + */ } From a5c7c2bb218207e98e1b0f82a14f393c64bc3a06 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 22 Aug 2024 11:46:12 +0100 Subject: [PATCH 23/42] WIP: ledger test new way to sign expressions --- Sources/KukaiCoreSwift/Models/LedgerWallet.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index 116f484..c5db49c 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -81,11 +81,11 @@ public class LedgerWallet: Wallet { } var bytesToSign: [UInt8] = [] - if isOperation { + //if isOperation { bytesToSign = bytes.addOperationWatermarkAndHash() ?? [] - } else { - bytesToSign = Sodium.shared.genericHash.hash(message: bytes, outputLength: 32) ?? [] - } + //} else { + // bytesToSign = Sodium.shared.genericHash.hash(message: bytes, outputLength: 32) ?? [] + //} guard let hexString = Sodium.shared.utils.bin2hex(bytesToSign) else { completion(Result.failure(KukaiError.internalApplicationError(error: WalletError.signatureError))) From 2d3bfad9553bda3c1b31e57ac5957c1c4e9ec728 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 22 Aug 2024 11:53:11 +0100 Subject: [PATCH 24/42] WIP: test ledger sign expression --- Sources/KukaiCoreSwift/Models/LedgerWallet.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index c5db49c..d6abbe7 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -81,11 +81,11 @@ public class LedgerWallet: Wallet { } var bytesToSign: [UInt8] = [] - //if isOperation { + if isOperation { bytesToSign = bytes.addOperationWatermarkAndHash() ?? [] - //} else { - // bytesToSign = Sodium.shared.genericHash.hash(message: bytes, outputLength: 32) ?? [] - //} + }/* else { + bytesToSign = Sodium.shared.genericHash.hash(message: bytes, outputLength: 32) ?? [] + }*/ guard let hexString = Sodium.shared.utils.bin2hex(bytesToSign) else { completion(Result.failure(KukaiError.internalApplicationError(error: WalletError.signatureError))) From 1711f654e1c1b5560cb58c118032d2da3bbdc702 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 22 Aug 2024 12:00:22 +0100 Subject: [PATCH 25/42] WIP: ledger sign expression --- .../KukaiCoreSwift/Models/LedgerWallet.swift | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index d6abbe7..7f973bc 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -75,26 +75,14 @@ public class LedgerWallet: Wallet { Please be careful when asking the Ledger to parse (passing in an operation), Ledgers have very limited display ability. Keep it to a single operation, not invoking a smart contract */ public func sign(_ hex: String, isOperation: Bool, completion: @escaping ((Result<[UInt8], KukaiError>) -> Void)) { - guard let bytes = Sodium.shared.utils.hex2bin(hex) else { - completion(Result.failure(KukaiError.internalApplicationError(error: WalletError.signatureError))) - return - } - - var bytesToSign: [UInt8] = [] + var hexToSign = hex if isOperation { - bytesToSign = bytes.addOperationWatermarkAndHash() ?? [] - }/* else { - bytesToSign = Sodium.shared.genericHash.hash(message: bytes, outputLength: 32) ?? [] - }*/ - - guard let hexString = Sodium.shared.utils.bin2hex(bytesToSign) else { - completion(Result.failure(KukaiError.internalApplicationError(error: WalletError.signatureError))) - return + hexToSign = "03"+hex } LedgerService.shared.connectTo(uuid: ledgerUUID) .flatMap { _ -> AnyPublisher in - return LedgerService.shared.sign(hex: hexString, parse: true) + return LedgerService.shared.sign(hex: hexToSign, parse: true) } .sink(onError: { error in completion(Result.failure(error)) From 0819449cf6606a95ff2b7434a50911538922c3cf Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 22 Aug 2024 12:11:54 +0100 Subject: [PATCH 26/42] clean up code, remove functions no longer necessary, remove tests that are no longer relevant --- .../KukaiCoreSwift/Models/LedgerWallet.swift | 48 +------------------ .../Services/ErrorHandlingServiceTests.swift | 2 +- .../Services/OperationServiceTests.swift | 22 --------- 3 files changed, 2 insertions(+), 70 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index 7f973bc..ed9d19c 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -70,9 +70,7 @@ public class LedgerWallet: Wallet { /** Sign a hex string. - If the string starts with "03" and is not 32 characters long, it will be treated as a watermarked operation and Ledger will be asked to parse + display the operation details. - Else it will be treated as an unknown operation and will simply display the Blake2b hash. - Please be careful when asking the Ledger to parse (passing in an operation), Ledgers have very limited display ability. Keep it to a single operation, not invoking a smart contract + If its an operation "03" will be prefix to the start, if not the hex will be passed directly to the ledger as it now supports parsing strings directly, but requires them to be unhashed */ public func sign(_ hex: String, isOperation: Bool, completion: @escaping ((Result<[UInt8], KukaiError>) -> Void)) { var hexToSign = hex @@ -98,50 +96,6 @@ public class LedgerWallet: Wallet { .store(in: &bag) } - /* - /** - Ledger can only parse operations under certain conditions. These conditions are not documented well. This function will attempt to determine whether the payload can be parsed or not, and returnt he appropriate string for the LedgerWallet sign function - It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error - If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead - */ - public func ledgerStringToSign(forgedHash: String, operationPayload: OperationPayload) -> String { - let watermarkedOp = "03" + forgedHash - let watermarkedBytes = Sodium.shared.utils.hex2bin(watermarkedOp) ?? [] - let blakeHash = Sodium.shared.genericHash.hash(message: watermarkedBytes, outputLength: 32) - let blakeHashString = blakeHash?.toHexString() ?? "" - - // Ledger can only parse operations under certain conditions. These conditions are not documented well. - // It seems to be able to parse the payload if it contains 1 operation, of the below types. Combining types (like Reveal + Transation) causes a parse error - // If the payload structure passes the conditions we are aware of, allow parsing to take place. If not, sign blake2b hash instead - var ledgerCanParse = false - if operationPayload.contents.count == 1, let first = operationPayload.contents.first, (first is OperationReveal || first is OperationDelegation || first is OperationTransaction) { - ledgerCanParse = true - } - - return watermarkedOp //(ledgerCanParse ? watermarkedOp : blakeHashString) - } - */ - - - /* - private func operationPayloadCanBeParsedByLedger(_ operationPayload: OperationPayload) -> Bool { - if operationPayload.contents.count == 1, let first = operationPayload.contents.first { - if first is OperationReveal || first is OperationDelegation { - return true - - } else if first is OperationTransaction, let transOp = first as? OperationTransaction, transOp.parameters == nil { - return true - } - } - - return false - } - */ - - - - - /** Function to extract the curve used to create the public key */ diff --git a/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift index 88cd623..6dacefe 100644 --- a/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift @@ -30,7 +30,7 @@ class ErrorHandlingServiceTests: XCTestCase { let error4 = KukaiError.internalApplicationError(error: URLError(URLError.unknown)) XCTAssert(error4.rpcErrorString == nil, error4.rpcErrorString ?? "-") - XCTAssert(error4.description == "Internal Application Error: The operation couldn’t be completed. (NSURLErrorDomain error -1.)", error4.description) + XCTAssert(error4.description == "Internal Application Error: Error Domain=NSURLErrorDomain Code=-1 \"(null)\"", error4.description) let error5 = KukaiError.systemError(subType: URLError(URLError.unknown)) XCTAssert(error5.rpcErrorString == nil, error5.rpcErrorString ?? "-") diff --git a/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift index 5a2c6f4..fe58b86 100644 --- a/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/OperationServiceTests.swift @@ -53,26 +53,4 @@ class OperationServiceTests: XCTestCase { wait(for: [expectation], timeout: 120) } - - /* - func testLedgerWithoutReveal() { - let stringToSign = operationService.ledgerStringToSign( - forgedHash: "43f597d84037e88354ed041cc6356f737cc6638691979bb64415451b58b4af2c6c00ad00bb6cbcfc497bffbaf54c23511c74dbeafb2d00ffde080000c0843d00005134b25890279835eb946e6369a3d719bc0d617700", - operationPayload: MockConstants.sendOperationPayload - ) - - // Should be an operation starting with "03" - XCTAssert(stringToSign == "0343f597d84037e88354ed041cc6356f737cc6638691979bb64415451b58b4af2c6c00ad00bb6cbcfc497bffbaf54c23511c74dbeafb2d00ffde080000c0843d00005134b25890279835eb946e6369a3d719bc0d617700", stringToSign) - } - - func testLedgerWithReveal() { - let stringToSign = operationService.ledgerStringToSign( - forgedHash: "43f597d84037e88354ed041cc6356f737cc6638691979bb64415451b58b4af2c6c00ad00bb6cbcfc497bffbaf54c23511c74dbeafb2d00ffde080000c0843d00005134b25890279835eb946e6369a3d719bc0d617700", - operationPayload: MockConstants.sendOperationWithRevealPayload - ) - - // Should be a 32 character long blake2b hash - XCTAssert(stringToSign == "a49d693a7bf5c22564bbb9368c94362ea64f07e5c5d1c443b63190ae5c85adf2", stringToSign) - } - */ } From 6134335dc3c85a20402705649caa2fbd655d663e Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Thu, 22 Aug 2024 16:01:57 +0100 Subject: [PATCH 27/42] - better handling of when ledger is disconnected - make sure ledger sign pulls in the correct derivation path --- .../KukaiCoreSwift/Models/LedgerWallet.swift | 4 ++-- .../Services/LedgerService.swift | 20 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index ed9d19c..1df8041 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -79,8 +79,8 @@ public class LedgerWallet: Wallet { } LedgerService.shared.connectTo(uuid: ledgerUUID) - .flatMap { _ -> AnyPublisher in - return LedgerService.shared.sign(hex: hexToSign, parse: true) + .flatMap { [weak self] _ -> AnyPublisher in + return LedgerService.shared.sign(hex: hexToSign, forDerivationPath: self?.derivationPath ?? HD.defaultDerivationPath, parse: true) } .sink(onError: { error in completion(Result.failure(error)) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index fa83f5a..a2fda59 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -265,7 +265,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele - returns: Publisher which will indicate true / false, or return an `KukaiError` if it can't connect to bluetooth */ public func connectTo(uuid: String) -> AnyPublisher { - if self.connectedDevice != nil, self.connectedDevice?.identifier.uuidString == uuid { + if self.connectedDevice != nil, self.connectedDevice?.identifier.uuidString == uuid, self.connectedDevice?.state == .connected { return AnyPublisher.just(true) } @@ -299,7 +299,11 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele - returns: a string if it can be found */ public func getConnectedDeviceUUID() -> String? { - return self.connectedDevice?.identifier.uuidString + if self.connectedDevice?.state == .connected { + return self.connectedDevice?.identifier.uuidString + } else { + return nil + } } /** @@ -441,6 +445,18 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele } } + public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { + Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") + self.connectedDevice = nil + self.deviceConnectedPublisher.send(false) + } + + public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, timestamp: CFAbsoluteTime, isReconnecting: Bool, error: (any Error)?) { + Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") + self.connectedDevice = nil + self.deviceConnectedPublisher.send(false) + } + /// CBCentralManagerDelegate function, must be marked public because of protocol definition public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard let characteristics = service.characteristics else { From da01da5ef85dedadd3f4b6c1310c3a22a026e813 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Fri, 23 Aug 2024 10:43:39 +0100 Subject: [PATCH 28/42] bug fix for metadata not being removed when deleting wallet --- .../Services/WalletCacheService.swift | 15 ++++++------ .../Services/WalletCacheServiceTests.swift | 23 +++++++++++++++---- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 8b96002..9b5b303 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -149,7 +149,7 @@ public class WalletCacheService { } // Update wallet metadata array, and then commit to disk - updateMetadataArray(forType: wallet.type, withNewArray: array, frorMeta: newMetadata) + updateMetadataArray(forType: wallet.type, withNewArray: array, forMeta: newMetadata) if encryptAndWriteWalletsToDisk(wallets: newWallets) && encryptAndWriteMetadataToDisk(newMetadata) == false { throw WalletCacheError.unableToEncryptAndWrite } else { @@ -174,18 +174,18 @@ public class WalletCacheService { } /// Helper method to take ina new sub array and update and existing reference, to reduce code complexity - private func updateMetadataArray(forType: WalletType, withNewArray: [WalletMetadata], frorMeta: WalletMetadataList) { + private func updateMetadataArray(forType: WalletType, withNewArray: [WalletMetadata], forMeta: WalletMetadataList) { switch forType { case .regular: - frorMeta.linearWallets = withNewArray + forMeta.linearWallets = withNewArray case .regularShifted: - frorMeta.linearWallets = withNewArray + forMeta.linearWallets = withNewArray case .hd: - frorMeta.hdWallets = withNewArray + forMeta.hdWallets = withNewArray case .social: - frorMeta.socialWallets = withNewArray + forMeta.socialWallets = withNewArray case .ledger: - frorMeta.ledgerWallets = withNewArray + forMeta.ledgerWallets = withNewArray } } @@ -258,6 +258,7 @@ public class WalletCacheService { } } + updateMetadataArray(forType: type, withNewArray: array, forMeta: newMetadata) return encryptAndWriteWalletsToDisk(wallets: newWallets) && encryptAndWriteMetadataToDisk(newMetadata) } diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index 2147414..5ee04a9 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -405,12 +405,25 @@ class WalletCacheServiceTests: XCTestCase { try walletCacheService.cache(wallet: ledgerWalletChild2, childOfIndex: 0, backedUp: true) try walletCacheService.cache(wallet: ledgerWalletChild3, childOfIndex: 0, backedUp: true, customDerivationPath: ledgerWalletChild3.derivationPath) - let list = walletCacheService.readMetadataFromDiskAndDecrypt() - let ledgers = list.ledgerWallets - let excludedCount = ledgers[0].childCountExcludingCustomDerivationPaths() - XCTAssert(ledgers.count == 1, ledgers.count.description) - XCTAssert(ledgers[0].children.count == 3, ledgers[0].children.count.description) + let list1 = walletCacheService.readMetadataFromDiskAndDecrypt() + let ledgers1 = list1.ledgerWallets + let excludedCount = ledgers1[0].childCountExcludingCustomDerivationPaths() + XCTAssert(ledgers1.count == 1, ledgers1.count.description) + XCTAssert(ledgers1[0].children.count == 3, ledgers1[0].children.count.description) XCTAssert(excludedCount == 2, excludedCount.description) + + + let _ = WalletCacheService().deleteWallet(withAddress: "tz1abc", parentIndex: nil) + let list2 = walletCacheService.readMetadataFromDiskAndDecrypt() + let ledgers2 = list2.ledgerWallets + XCTAssert(ledgers2.count == 0, ledgers2.count.description) + + let testWalletsGone1 = WalletCacheService().fetchWallet(forAddress: "tz1abc") + XCTAssert(testWalletsGone1 == nil) + + let testWalletsGone2 = WalletCacheService().fetchWallet(forAddress: "tz1def") + XCTAssert(testWalletsGone2 == nil) + } catch let error { XCTFail("Should not error: \(error)") } From 2477fa0faefdee7c73f64211fc94906ee0159e3f Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Mon, 23 Sep 2024 17:41:25 +0100 Subject: [PATCH 29/42] - remove "custom derivation path" - add derivation path to metadata --- .../Models/WalletMetadata.swift | 11 +++----- .../Services/WalletCacheService.swift | 17 ++++++++---- .../Models/TzKTTransactionTests.swift | 2 +- .../Services/WalletCacheServiceTests.swift | 27 +++++++++---------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift index 370439d..edfe6ae 100644 --- a/Sources/KukaiCoreSwift/Models/WalletMetadata.swift +++ b/Sources/KukaiCoreSwift/Models/WalletMetadata.swift @@ -265,6 +265,7 @@ public class WalletMetadataList: Codable, Hashable { /// Object to store UI related info about wallets, seperated from the wallet object itself to avoid issues merging together public class WalletMetadata: Codable, Hashable { public var address: String + public var derivationPath: String? public var hdWalletGroupName: String? public var walletNickname: String? public var socialUsername: String? @@ -278,7 +279,6 @@ public class WalletMetadata: Codable, Hashable { public var isWatchOnly: Bool public var bas58EncodedPublicKey: String public var backedUp: Bool - public var customDerivationPath: String? public func hasMainnetDomain() -> Bool { return (mainnetDomains ?? []).count > 0 @@ -320,13 +320,9 @@ public class WalletMetadata: Codable, Hashable { } } - public func childCountExcludingCustomDerivationPaths() -> Int { - let excluded = children.filter { $0.customDerivationPath == nil } - return excluded.count - } - - public init(address: String, hdWalletGroupName: String?, walletNickname: String? = nil, socialUsername: String? = nil, socialUserId: String? = nil, mainnetDomains: [TezosDomainsReverseRecord]? = nil, ghostnetDomains: [TezosDomainsReverseRecord]? = nil, socialType: TorusAuthProvider? = nil, type: WalletType, children: [WalletMetadata], isChild: Bool, isWatchOnly: Bool, bas58EncodedPublicKey: String, backedUp: Bool, customDerivationPath: String?) { + public init(address: String, derivationPath: String?, hdWalletGroupName: String?, walletNickname: String? = nil, socialUsername: String? = nil, socialUserId: String? = nil, mainnetDomains: [TezosDomainsReverseRecord]? = nil, ghostnetDomains: [TezosDomainsReverseRecord]? = nil, socialType: TorusAuthProvider? = nil, type: WalletType, children: [WalletMetadata], isChild: Bool, isWatchOnly: Bool, bas58EncodedPublicKey: String, backedUp: Bool) { self.address = address + self.derivationPath = derivationPath self.hdWalletGroupName = hdWalletGroupName self.walletNickname = walletNickname self.socialUsername = socialUsername @@ -340,7 +336,6 @@ public class WalletMetadata: Codable, Hashable { self.isWatchOnly = isWatchOnly self.bas58EncodedPublicKey = bas58EncodedPublicKey self.backedUp = backedUp - self.customDerivationPath = customDerivationPath } public static func == (lhs: WalletMetadata, rhs: WalletMetadata) -> Bool { diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 9b5b303..30887ea 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -85,7 +85,7 @@ public class WalletCacheService { - Parameter childOfIndex: An optional `Int` to denote the index of the HD wallet that this wallet is a child of - Returns: Bool, indicating if the storage was successful or not */ - public func cache(wallet: T, childOfIndex: Int?, backedUp: Bool, customDerivationPath: String? = nil) throws { + public func cache(wallet: T, childOfIndex: Int?, backedUp: Bool) throws { guard let existingWallets = readWalletsFromDiskAndDecrypt() else { Logger.walletCache.error("cache - Unable to cache wallet, as can't decrypt existing wallets") throw WalletCacheError.unableToDecrypt @@ -102,6 +102,13 @@ public class WalletCacheService { let newMetadata = readMetadataFromDiskAndDecrypt() var array = metadataArray(forType: wallet.type, fromMeta: newMetadata) + var path: String? = nil + if wallet.type == .hd, let w = wallet as? HDWallet { + path = w.derivationPath + } else if wallet.type == .ledger, let w = wallet as? LedgerWallet { + path = w.derivationPath + } + if let index = childOfIndex { // If child index is present, update the correct sub array to include this new item, checking forst that we have the correct details @@ -110,7 +117,7 @@ public class WalletCacheService { throw WalletCacheError.requestedIndexTooHigh } - array[index].children.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, type: wallet.type, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) + array[index].children.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, type: wallet.type, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) } else if wallet.type == .hd || wallet.type == .ledger { @@ -135,17 +142,17 @@ public class WalletCacheService { newNumber = array.count + 1 } - array.append(WalletMetadata(address: wallet.address, hdWalletGroupName: "\(groupNameStart)\(newNumber)", walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) + array.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: "\(groupNameStart)\(newNumber)", walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) } else if let torusWallet = wallet as? TorusWallet { // If social, cast and fetch special attributes - array.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: torusWallet.socialUsername, socialUserId: torusWallet.socialUserId, socialType: torusWallet.authProvider, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) + array.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: nil, walletNickname: nil, socialUsername: torusWallet.socialUsername, socialUserId: torusWallet.socialUserId, socialType: torusWallet.authProvider, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) } else { // Else, add basic wallet to the list its supposed to go to - array.append(WalletMetadata(address: wallet.address, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp, customDerivationPath: customDerivationPath)) + array.append(WalletMetadata(address: wallet.address, derivationPath: path, hdWalletGroupName: nil, walletNickname: nil, socialUsername: nil, socialType: nil, type: wallet.type, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: wallet.publicKeyBase58encoded(), backedUp: backedUp)) } // Update wallet metadata array, and then commit to disk diff --git a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift index 98e9b31..2c2fd62 100644 --- a/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/TzKTTransactionTests.swift @@ -78,7 +78,7 @@ final class TzKTTransactionTests: XCTestCase { } func testPlaceholders() { - let source = WalletMetadata(address: "tz1abc", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + let source = WalletMetadata(address: "tz1abc", derivationPath: nil, 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, baker: nil, kind: nil) diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index 5ee04a9..712d8d4 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -249,7 +249,7 @@ class WalletCacheServiceTests: XCTestCase { func testMetadata() { let mainentDomain = [TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.tez", address: "tz1abc123"))] let ghostnetDomain = [TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.gho", address: "tz1abc123"))] - let metadata1 = WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: mainentDomain, ghostnetDomains: ghostnetDomain, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + let metadata1 = WalletMetadata(address: "tz1abc123", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: mainentDomain, ghostnetDomains: ghostnetDomain, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) XCTAssert(metadata1.hasMainnetDomain()) XCTAssert(metadata1.hasGhostnetDomain()) @@ -264,7 +264,7 @@ class WalletCacheServiceTests: XCTestCase { - let metadata2 = WalletMetadata(address: "tz1def456", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + let metadata2 = WalletMetadata(address: "tz1def456", derivationPath: nil, hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) XCTAssert(!metadata2.hasMainnetDomain()) XCTAssert(!metadata2.hasGhostnetDomain()) @@ -273,20 +273,20 @@ class WalletCacheServiceTests: XCTestCase { func testMetadataList() { let mainentDomain = TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.tez", address: "tz1abc123")) let ghostnetDomain = TezosDomainsReverseRecord(id: "123", address: "tz1abc123", owner: "tz1abc123", expiresAtUtc: nil, domain: TezosDomainsDomain(name: "blah.gho", address: "tz1abc123")) - let child = WalletMetadata(address: "tz1child", hdWalletGroupName: nil, type: .hd, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) - let updatedWatch = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "blah", backedUp: true, customDerivationPath: nil) + let child = WalletMetadata(address: "tz1child", derivationPath: nil, hdWalletGroupName: nil, type: .hd, children: [], isChild: true, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) + let updatedWatch = WalletMetadata(address: "tz1jkl", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "blah", backedUp: true) let hd: [WalletMetadata] = [ - WalletMetadata(address: "tz1abc123", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [child], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + WalletMetadata(address: "tz1abc123", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [child], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) ] let social: [WalletMetadata] = [ - WalletMetadata(address: "tz1def", hdWalletGroupName: nil, socialUsername: "test@gmail.com", socialType: .google, type: .social, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + WalletMetadata(address: "tz1def", derivationPath: nil, hdWalletGroupName: nil, socialUsername: "test@gmail.com", socialType: .google, type: .social, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) ] let linear: [WalletMetadata] = [ - WalletMetadata(address: "tz1ghi", hdWalletGroupName: nil, type: .regular, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + WalletMetadata(address: "tz1ghi", derivationPath: nil, hdWalletGroupName: nil, type: .regular, children: [], isChild: false, isWatchOnly: false, bas58EncodedPublicKey: "", backedUp: true) ] let watch: [WalletMetadata] = [ - WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + WalletMetadata(address: "tz1jkl", derivationPath: nil, hdWalletGroupName: nil, type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) ] let list = WalletMetadataList(socialWallets: social, hdWallets: hd, linearWallets: linear, ledgerWallets: [], watchWallets: watch) @@ -333,7 +333,7 @@ class WalletCacheServiceTests: XCTestCase { func testWatchWallet() { XCTAssert(walletCacheService.deleteAllCacheAndKeys()) - let watchWallet = WalletMetadata(address: "tz1jkl", hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + let watchWallet = WalletMetadata(address: "tz1jkl", derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) @@ -354,7 +354,7 @@ class WalletCacheServiceTests: XCTestCase { XCTFail("Should not error: \(error)") } - let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) @@ -368,7 +368,7 @@ class WalletCacheServiceTests: XCTestCase { XCTAssert(walletCacheService.deleteAllCacheAndKeys()) // Add watch and confirm - let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true, customDerivationPath: nil) + let watchWallet = WalletMetadata(address: MockConstants.defaultLinearWallet.address, derivationPath: nil, hdWalletGroupName: nil, mainnetDomains: [], ghostnetDomains: [], type: .hd, children: [], isChild: false, isWatchOnly: true, bas58EncodedPublicKey: "", backedUp: true) do { try walletCacheService.cacheWatchWallet(metadata: watchWallet) @@ -403,14 +403,13 @@ class WalletCacheServiceTests: XCTestCase { try walletCacheService.cache(wallet: ledgerWallet, childOfIndex: nil, backedUp: true) try walletCacheService.cache(wallet: ledgerWalletChild1, childOfIndex: 0, backedUp: true) try walletCacheService.cache(wallet: ledgerWalletChild2, childOfIndex: 0, backedUp: true) - try walletCacheService.cache(wallet: ledgerWalletChild3, childOfIndex: 0, backedUp: true, customDerivationPath: ledgerWalletChild3.derivationPath) + try walletCacheService.cache(wallet: ledgerWalletChild3, childOfIndex: 0, backedUp: true) let list1 = walletCacheService.readMetadataFromDiskAndDecrypt() let ledgers1 = list1.ledgerWallets - let excludedCount = ledgers1[0].childCountExcludingCustomDerivationPaths() XCTAssert(ledgers1.count == 1, ledgers1.count.description) XCTAssert(ledgers1[0].children.count == 3, ledgers1[0].children.count.description) - XCTAssert(excludedCount == 2, excludedCount.description) + XCTAssert(ledgers1[0].children.last?.derivationPath == "m/44'/1729'/147'/62'", ledgers1[0].children.last?.derivationPath ?? "-") let _ = WalletCacheService().deleteWallet(withAddress: "tz1abc", parentIndex: nil) From d5840c7169b92c74b6111f3b16321d32a4f94d30 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Mon, 23 Sep 2024 21:06:12 +0100 Subject: [PATCH 30/42] change ledger group name --- Sources/KukaiCoreSwift/Services/WalletCacheService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 30887ea..6492b60 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -127,7 +127,7 @@ public class WalletCacheService { case .hd: groupNameStart = "HD Wallet " case .ledger: - groupNameStart = "Ledger Wallet " + groupNameStart = "Ledger Device " case .social, .regular, .regularShifted: groupNameStart = "" } From 08b109ac450021172ce67ec625fc5ce6dafd7d38 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Mon, 21 Oct 2024 12:38:10 +0100 Subject: [PATCH 31/42] make sure requestedUUID gets reset so listen for devices doesn't get blcoked --- Sources/KukaiCoreSwift/Services/LedgerService.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index a2fda59..c5fb78f 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -292,6 +292,8 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele if let device = self.connectedDevice { self.centralManager?.cancelPeripheralConnection(device) } + + requestedUUID = nil } /** @@ -425,6 +427,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { Logger.ledger.info("Failed to connect to \(peripheral.name ?? ""), \(peripheral.identifier.uuidString)") self.connectedDevice = nil + self.requestedUUID = nil self.deviceConnectedPublisher.send(false) } @@ -433,6 +436,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele guard let services = peripheral.services else { Logger.ledger.info("Unable to locate services for: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil + self.requestedUUID = nil self.deviceConnectedPublisher.send(false) return } @@ -448,12 +452,14 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil + self.requestedUUID = nil self.deviceConnectedPublisher.send(false) } public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, timestamp: CFAbsoluteTime, isReconnecting: Bool, error: (any Error)?) { Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil + self.requestedUUID = nil self.deviceConnectedPublisher.send(false) } @@ -462,6 +468,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele guard let characteristics = service.characteristics else { Logger.ledger.info("Unable to locate characteristics for: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil + self.requestedUUID = nil self.deviceConnectedPublisher.send(false) return } From c7745e56ee2a26663d33bfcc8565e3d2fdb9df03 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Mon, 21 Oct 2024 12:45:05 +0100 Subject: [PATCH 32/42] empty device list after scan stop, to allow notifications to come in again --- Sources/KukaiCoreSwift/Services/LedgerService.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index c5fb78f..7211fe2 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -257,6 +257,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele */ public func stopListening() { self.centralManager?.stopScan() + self.deviceList = [:] self.deviceListPublisher.send(completion: .finished) } From 5c30006566402f13b15c01b3dee38caa6f2db960 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Mon, 21 Oct 2024 13:59:03 +0100 Subject: [PATCH 33/42] - fix test - reset ledger characteristics --- Sources/KukaiCoreSwift/Services/LedgerService.swift | 10 ++++++++++ .../Models/CurrentDeviceTests.swift | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index 7211fe2..deacc98 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -429,6 +429,8 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele Logger.ledger.info("Failed to connect to \(peripheral.name ?? ""), \(peripheral.identifier.uuidString)") self.connectedDevice = nil self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) } @@ -438,6 +440,8 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele Logger.ledger.info("Unable to locate services for: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) return } @@ -454,6 +458,8 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) } @@ -461,6 +467,8 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele Logger.ledger.info("Disconnected: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) } @@ -470,6 +478,8 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele Logger.ledger.info("Unable to locate characteristics for: \(peripheral.name ?? ""), \(peripheral.identifier.uuidString). Error: \(error)") self.connectedDevice = nil self.requestedUUID = nil + self.notifyCharacteristic = nil + self.writeCharacteristic = nil self.deviceConnectedPublisher.send(false) return } diff --git a/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift b/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift index c0d07ef..4841c37 100644 --- a/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift @@ -23,6 +23,6 @@ class CurrentDeviceTests: XCTestCase { } func testBiometrics() { - XCTAssert(CurrentDevice.biometricTypeSupported() == .none) + XCTAssert(CurrentDevice.biometricTypeSupported() == .faceID) } } From f770faee2bd4115bb7871bd87641587ac3434f70 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Mon, 21 Oct 2024 15:27:18 +0100 Subject: [PATCH 34/42] - add new walletCache function to migrate ledgers to new UUID - add new test --- .../Services/WalletCacheService.swift | 23 ++++++++++++ .../Services/WalletCacheServiceTests.swift | 35 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift index 6492b60..77f871c 100644 --- a/Sources/KukaiCoreSwift/Services/WalletCacheService.swift +++ b/Sources/KukaiCoreSwift/Services/WalletCacheService.swift @@ -292,6 +292,29 @@ public class WalletCacheService { return cacheItems[address] } + /** + Migrate a LedgerWallet and its children to a new physical device, denoted by a new UUID + */ + public func migrateLedger(metadata: WalletMetadata, toNewUUID: String) -> Bool { + guard let cacheItems = readWalletsFromDiskAndDecrypt() else { + Logger.walletCache.error("Unable to read wallet items") + return false + } + + var addressesToMigrate = [metadata.address] + addressesToMigrate.append(contentsOf: metadata.children.map({ $0.address })) + + for address in addressesToMigrate { + guard let ledgerWallet = cacheItems[address] as? LedgerWallet else { + return false + } + + ledgerWallet.ledgerUUID = toNewUUID + } + + return encryptAndWriteWalletsToDisk(wallets: cacheItems) + } + /** Delete the cached files and the assoicate keys used to encrypt it - Returns: Bool, indicating if the process was successful or not diff --git a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift index 712d8d4..3dcb55a 100644 --- a/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Services/WalletCacheServiceTests.swift @@ -427,4 +427,39 @@ class WalletCacheServiceTests: XCTestCase { XCTFail("Should not error: \(error)") } } + + func testLedgerWalletMigrate() { + XCTAssert(walletCacheService.deleteAllCacheAndKeys()) + + let ledgerWallet = LedgerWallet(address: "tz1abc", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + let ledgerWalletChild1 = LedgerWallet(address: "tz1def", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + let ledgerWalletChild2 = LedgerWallet(address: "tz1ghi", publicKey: "edpks1234", derivationPath: HD.defaultDerivationPath, curve: .ed25519, ledgerUUID: "blah1")! + + do { + try walletCacheService.cache(wallet: ledgerWallet, childOfIndex: nil, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild1, childOfIndex: 0, backedUp: true) + try walletCacheService.cache(wallet: ledgerWalletChild2, childOfIndex: 0, backedUp: true) + + let list = walletCacheService.readMetadataFromDiskAndDecrypt() + guard let ledger = list.ledgerWallets.first else { + XCTFail("Ledger list empty") + return + } + + XCTAssert(walletCacheService.migrateLedger(metadata: ledger, toNewUUID: "migrated")) + + let ledgerParent = walletCacheService.fetchWallet(forAddress: ledgerWallet.address) as? LedgerWallet + XCTAssert(ledgerParent?.ledgerUUID == "migrated") + + let ledgerChild1 = walletCacheService.fetchWallet(forAddress: ledgerWalletChild1.address) as? LedgerWallet + XCTAssert(ledgerChild1?.ledgerUUID == "migrated") + + let ledgerChild2 = walletCacheService.fetchWallet(forAddress: ledgerWalletChild2.address) as? LedgerWallet + XCTAssert(ledgerChild2?.ledgerUUID == "migrated") + + + } catch let error { + XCTFail("Should not error: \(error)") + } + } } From d6cd2708de8520f53a0450ddb5e2b64fae7a3709 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Wed, 23 Oct 2024 15:56:02 +0100 Subject: [PATCH 35/42] prevent the closing of the device connected subject while the user might be actively trying to fix the problem --- Sources/KukaiCoreSwift/Services/LedgerService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/LedgerService.swift b/Sources/KukaiCoreSwift/Services/LedgerService.swift index deacc98..b468d45 100644 --- a/Sources/KukaiCoreSwift/Services/LedgerService.swift +++ b/Sources/KukaiCoreSwift/Services/LedgerService.swift @@ -273,7 +273,7 @@ public class LedgerService: NSObject, CBPeripheralDelegate, CBCentralManagerDele self.setupBluetoothConnection() .sink { [weak self] value in if !value { - self?.deviceConnectedPublisher.send(completion: .failure(KukaiError.unknown())) + self?.deviceConnectedPublisher.send(false) } self?.requestedUUID = uuid From 5902d3525484ce4e26a19de373d75d7e5362542f Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Thu, 24 Oct 2024 11:29:48 +0100 Subject: [PATCH 36/42] - bump torus version - update CI to use latest Xcode --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/unit-test.yml | 4 ++-- Package.swift | 2 +- Sources/KukaiCoreSwift/Models/LedgerWallet.swift | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 90419f0..197d077 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -10,10 +10,10 @@ on: jobs: deploy: name: Running unit tests - runs-on: macos-14 + runs-on: macos-15 steps: - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_15.2.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_16.app/Contents/Developer' - name: Checkout repository uses: actions/checkout@v4.1.1 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 8ef4691..4a21451 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -12,11 +12,11 @@ on: jobs: deploy: name: Running unit tests - runs-on: macos-14 + runs-on: macos-15 steps: - name: Select Xcode version #run: sudo xcode-select -s '/Applications/Xcode_14.3.1.app/Contents/Developer' - run: sudo xcode-select -s '/Applications/Xcode_15.2.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_16.app/Contents/Developer' - name: Checkout repository uses: actions/checkout@v4.1.1 diff --git a/Package.swift b/Package.swift index bd8e6b0..354041d 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( ], dependencies: [ .package(name: "KukaiCryptoSwift", url: "https://github.com/kukai-wallet/kukai-crypto-swift", from: "1.0.23" /*.branch("develop")*/), - .package(name: "CustomAuth", url: "https://github.com/torusresearch/customauth-swift-sdk", from: "10.0.1"), + .package(name: "CustomAuth", url: "https://github.com/torusresearch/customauth-swift-sdk", from: "10.0.2"), .package(name: "SignalRClient", url: "https://github.com/moozzyk/SignalR-Client-Swift", from: "0.8.0"), .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.19.3") ], diff --git a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift index 1df8041..3b6aee0 100644 --- a/Sources/KukaiCoreSwift/Models/LedgerWallet.swift +++ b/Sources/KukaiCoreSwift/Models/LedgerWallet.swift @@ -108,6 +108,6 @@ public class LedgerWallet: Wallet { */ public func publicKeyBase58encoded() -> String { let publicKeyData = Data(hexString: publicKey) ?? Data() - return Base58Check.encode(message: publicKeyData.bytes, prefix: Prefix.Keys.Ed25519.public) + return Base58Check.encode(message: publicKeyData.bytes(), prefix: Prefix.Keys.Ed25519.public) } } From 425ece07fc368b83677b349bebd09f994f07207c Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Thu, 24 Oct 2024 11:37:06 +0100 Subject: [PATCH 37/42] CI: bump versions --- .github/workflows/codeql.yml | 2 +- .github/workflows/docs-and-deploy.yml | 2 +- .github/workflows/unit-test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 197d077..ff47be5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: languages: "swift" - name: Test - run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15" + run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=18.0,name=iPhone 16" - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docs-and-deploy.yml b/.github/workflows/docs-and-deploy.yml index 049bc81..6be4218 100644 --- a/.github/workflows/docs-and-deploy.yml +++ b/.github/workflows/docs-and-deploy.yml @@ -5,7 +5,7 @@ on: - main jobs: build: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4.1.1 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 4a21451..e14472b 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -62,7 +62,7 @@ jobs: - name: Test #run: xcodebuild test -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=16.4,name=iPhone 14" -enableCodeCoverage YES -resultBundlePath "~/xcode-$NOW.xcresult" - run: xcodebuild test -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15" -enableCodeCoverage YES -resultBundlePath "~/xcode-$NOW.xcresult" + run: xcodebuild test -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=18.0,name=iPhone 16" -enableCodeCoverage YES -resultBundlePath "~/xcode-$NOW.xcresult" From 4d544db20ee385b8b7f3c85129846a691908de36 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Thu, 24 Oct 2024 11:46:02 +0100 Subject: [PATCH 38/42] CI: remove test that is now unreliable on Xcode 16. Device settings now persist, causing differences between local and remote --- Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift b/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift index 4841c37..28ddaaa 100644 --- a/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift +++ b/Tests/KukaiCoreSwiftTests/Models/CurrentDeviceTests.swift @@ -21,8 +21,4 @@ class CurrentDeviceTests: XCTestCase { func testCurrentDevice() { XCTAssert(CurrentDevice.isSimulator == true) } - - func testBiometrics() { - XCTAssert(CurrentDevice.biometricTypeSupported() == .faceID) - } } From 6fb2c01cb072f876f39b23114496cca4a1b62e9d Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Thu, 24 Oct 2024 12:11:15 +0100 Subject: [PATCH 39/42] CI: commenting out codeQl for now as it doesn't work correctly on macos-15/xcode 16 --- .github/workflows/codeql.yml | 70 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ff47be5..edd7b96 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,35 +1,35 @@ -name: CodeQl -on: - push: - branches: - - develop - pull_request: - branches: - - develop - -jobs: - deploy: - name: Running unit tests - runs-on: macos-15 - steps: - - name: Select Xcode version - run: sudo xcode-select -s '/Applications/Xcode_16.app/Contents/Developer' - - - name: Checkout repository - uses: actions/checkout@v4.1.1 - - - name: Get current date - run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "swift" - - - name: Test - run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=18.0,name=iPhone 16" - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:swift" \ No newline at end of file +#name: CodeQl +#on: +# push: +# branches: +# - develop +# pull_request: +# branches: +# - develop +# +#jobs: +# deploy: +# name: Running unit tests +# runs-on: macos-15 +# steps: +# - name: Select Xcode version +# run: sudo xcode-select -s '/Applications/Xcode_16.app/Contents/Developer' +# +# - name: Checkout repository +# uses: actions/checkout@v4.1.1 +# +# - name: Get current date +# run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV +# +# - name: Initialize CodeQL +# uses: github/codeql-action/init@v3 +# with: +# languages: "swift" +# +# - name: Test +# run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=18.0,name=iPhone 16" -resultBundlePath "~/xcode-$NOW.xcresult" +# +# - name: Perform CodeQL Analysis +# uses: github/codeql-action/analyze@v3 +# with: +# category: "/language:swift" \ No newline at end of file From 9271a03901e25ac6699a4f748f4f6c7aeecd7064 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Thu, 24 Oct 2024 12:13:12 +0100 Subject: [PATCH 40/42] CI: re-adding codeQL and running on macos-14 because github actions complains if theres no jobs for some reason --- .github/workflows/codeql.yml | 70 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index edd7b96..25a5f1d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,35 +1,35 @@ -#name: CodeQl -#on: -# push: -# branches: -# - develop -# pull_request: -# branches: -# - develop -# -#jobs: -# deploy: -# name: Running unit tests -# runs-on: macos-15 -# steps: -# - name: Select Xcode version -# run: sudo xcode-select -s '/Applications/Xcode_16.app/Contents/Developer' -# -# - name: Checkout repository -# uses: actions/checkout@v4.1.1 -# -# - name: Get current date -# run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV -# -# - name: Initialize CodeQL -# uses: github/codeql-action/init@v3 -# with: -# languages: "swift" -# -# - name: Test -# run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=18.0,name=iPhone 16" -resultBundlePath "~/xcode-$NOW.xcresult" -# -# - name: Perform CodeQL Analysis -# uses: github/codeql-action/analyze@v3 -# with: -# category: "/language:swift" \ No newline at end of file +name: CodeQl +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + deploy: + name: Running unit tests + runs-on: macos-14 + steps: + - name: Select Xcode version + run: sudo xcode-select -s '/Applications/Xcode_15.2.app/Contents/Developer' + + - name: Checkout repository + uses: actions/checkout@v4.1.1 + + - name: Get current date + run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: "swift" + + - name: Test + run: xcodebuild -scheme KukaiCoreSwift -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15" -resultBundlePath "~/xcode-$NOW.xcresult" + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:swift" \ No newline at end of file From 1cee51f685524981502e253bcce1f645160d5b92 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Fri, 1 Nov 2024 15:29:53 +0000 Subject: [PATCH 41/42] - add network service function to send a HTTP DELETE - clear facebook auth token after a successful login to allow users to login with multiple accounts --- .../Services/NetworkService.swift | 36 +++++++++++++++++++ .../Services/TorusAuthService.swift | 17 ++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Sources/KukaiCoreSwift/Services/NetworkService.swift b/Sources/KukaiCoreSwift/Services/NetworkService.swift index 3fabf40..f51fd5d 100644 --- a/Sources/KukaiCoreSwift/Services/NetworkService.swift +++ b/Sources/KukaiCoreSwift/Services/NetworkService.swift @@ -216,6 +216,42 @@ public class NetworkService { }.eraseToAnyPublisher() } + /** + Send a HTTP DELETE to a given URL + */ + public func delete(url: URL, completion: @escaping ((Result) -> Void)) { + var request = URLRequest(url: url) + request.addValue("application/json", forHTTPHeaderField: "Accept") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "DELETE" + + urlSession.dataTask(with: request) { (data, response, error) in + if let err = error { + completion(Result.failure(KukaiError.internalApplicationError(error: err))) + } else { + completion(Result.success(true)) + } + }.resume() + NetworkService.logRequestStart(loggingConfig: loggingConfig, fullURL: url) + } + + /** + Send a HTTP DELETE to a given URL + */ + public func delete(url: URL) -> AnyPublisher { + return Future { [weak self] promise in + self?.delete(url: url, completion: { result in + guard let output = try? result.get() else { + let error = (try? result.getError()) ?? KukaiError.unknown() + promise(.failure(error)) + return + } + + promise(.success(output)) + }) + }.eraseToAnyPublisher() + } + func checkForRPCOperationErrors(parsedResponse: Any, withRequestURL: URL?, requestPayload: Data?, responsePayload: Data?, httpStatusCode: Int?) -> KukaiError? { var operations: [OperationResponse] = [] diff --git a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift index ac2f966..a64c80f 100644 --- a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift +++ b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift @@ -250,21 +250,22 @@ public class TorusAuthService: NSObject { completion(Result.failure(KukaiError.internalApplicationError(error: TorusAuthError.missingVerifier))) } + let accessToken = data.userInfo["accessToken"] as? String // Twitter API doesn't give us the bloody "@" handle for some reason. Fetch that first and overwrite the username property with the handle, if found if authType == .twitter { twitterHandleLookup(id: userId ?? "") { [weak self] result in switch result { case .success(let actualUsername): - self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: actualUsername, userId: userId, profile: profile, completion: completion) + self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: actualUsername, userId: userId, profile: profile, accessToken: accessToken, completion: completion) case .failure(_): - self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, completion: completion) + self?.createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, accessToken: accessToken, completion: completion) } } } else { - createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, completion: completion) + createTorusWalletAndContinue(pk: pk, authType: authType, username: username, userId: userId, profile: profile, accessToken: accessToken, completion: completion) } } catch { @@ -275,14 +276,20 @@ public class TorusAuthService: NSObject { } } - private func createTorusWalletAndContinue(pk: String?, authType: TorusAuthProvider, username: String?, userId: String?, profile: String?, completion: @escaping ((Result) -> Void)) { + private func createTorusWalletAndContinue(pk: String?, authType: TorusAuthProvider, username: String?, userId: String?, profile: String?, accessToken: String?, completion: @escaping ((Result) -> Void)) { guard let privateKeyString = pk, let wallet = TorusWallet(authProvider: authType, username: username, userId: userId, profilePicture: profile, torusPrivateKey: privateKeyString) else { Logger.torus.error("Error torus contained no, or invlaid private key") completion(Result.failure(KukaiError.internalApplicationError(error: TorusAuthError.invalidTorusResponse))) return } - completion(Result.success(wallet)) + if authType == .facebook, let token = accessToken, let url = URL(string: "https://graph.facebook.com/me/permissions?access_token=\(token)") { + networkService.delete(url: url) { result in + completion(Result.success(wallet)) + } + } else { + completion(Result.success(wallet)) + } } From e7fe6e2e8d95feffbd8fd81bf4eeabef1b372a37 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Fri, 1 Nov 2024 15:40:16 +0000 Subject: [PATCH 42/42] - fix key name --- Sources/KukaiCoreSwift/Services/TorusAuthService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift index a64c80f..0390ca8 100644 --- a/Sources/KukaiCoreSwift/Services/TorusAuthService.swift +++ b/Sources/KukaiCoreSwift/Services/TorusAuthService.swift @@ -250,7 +250,7 @@ public class TorusAuthService: NSObject { completion(Result.failure(KukaiError.internalApplicationError(error: TorusAuthError.missingVerifier))) } - let accessToken = data.userInfo["accessToken"] as? String + let accessToken = data.userInfo["access_token"] as? String // Twitter API doesn't give us the bloody "@" handle for some reason. Fetch that first and overwrite the username property with the handle, if found if authType == .twitter {