diff --git a/IntegrationTests/Dexter/DexterExchangeClientIntegrationTests.swift b/IntegrationTests/Dexter/DexterExchangeClientIntegrationTests.swift new file mode 100644 index 00000000..e7838759 --- /dev/null +++ b/IntegrationTests/Dexter/DexterExchangeClientIntegrationTests.swift @@ -0,0 +1,182 @@ +// Copyright Keefer Taylor, 2019. + +@testable import TezosKit +import XCTest + +/// Integration tests to run against a DEXter Exchange Contract. These tests require a live alphanet node. +/// +/// To get an alphanet node running locally, follow instructions here: +/// https://tezos.gitlab.io/alphanet/introduction/howtoget.html +/// +/// These tests are not hermetic and may fail for a number or reasons, such as: +/// - Insufficient balance in account. +/// - Adverse network conditions. +/// +/// Before running the tests, you should make sure that there's sufficient tokens in the owners account (which is +/// tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW) and liquidity in the exchange: +/// Exchange: https://alphanet.tzscan.io/KT18dHMg7xWwRvo2TA9DSkcPkaG3AkDyEeKB +/// Address: https://alphanet.tzscan.io/tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW + +extension Address { + public static let exchangeContractAddress = "KT18dHMg7xWwRvo2TA9DSkcPkaG3AkDyEeKB" +} + +class DexterExchangeClientIntegrationTests: XCTestCase { + public var nodeClient = TezosNodeClient() + public var exchangeClient = DexterExchangeClient(exchangeContractAddress: "") + + public override func setUp() { + super.setUp() + + let nodeClient = TezosNodeClient(remoteNodeURL: .nodeURL) + exchangeClient = DexterExchangeClient( + exchangeContractAddress: .exchangeContractAddress, + tezosNodeClient: nodeClient + ) + } + + public func testGetBalanceTez() { + let completionExpectation = XCTestExpectation(description: "Completion called") + + exchangeClient.getExchangeBalanceTez { result in + guard case let .success(balance) = result else { + XCTFail() + return + } + + XCTAssert(balance > Tez.zeroBalance) + completionExpectation.fulfill() + } + wait(for: [ completionExpectation ], timeout: .expectationTimeout) + } + + public func testGetBalanceTokens() { + let completionExpectation = XCTestExpectation(description: "Completion called") + + exchangeClient.getExchangeBalanceTokens(tokenContractAddress: .tokenContractAddress) { result in + guard case let .success(balance) = result else { + XCTFail() + return + } + + XCTAssert(balance > 0) + completionExpectation.fulfill() + } + wait(for: [ completionExpectation ], timeout: .expectationTimeout) + } + + public func testGetExchangeLiquidity() { + let completionExpectation = XCTestExpectation(description: "Completion called") + + exchangeClient.getExchangeLiquidity { result in + guard case let .success(liquidity) = result else { + XCTFail() + return + } + + XCTAssert(liquidity > 0) + completionExpectation.fulfill() + } + wait(for: [ completionExpectation ], timeout: .expectationTimeout) + } + + public func testAddLiquidity() { + let completionExpectation = XCTestExpectation(description: "Completion called") + + let deadline = Date().addingTimeInterval(24 * 60 * 60) // 24 hours in the future + exchangeClient.addLiquidity( + from: Wallet.testWallet.address, + amount: Tez(10.0), + signatureProvider: Wallet.testWallet, + minLiquidity: 1, + maxTokensDeposited: 10, + deadline: deadline + ) { result in + switch result { + case .failure(let error): + print(error) + XCTFail() + case .success(let hash): + print(hash) + completionExpectation.fulfill() + } + } + + wait(for: [ completionExpectation ], timeout: .expectationTimeout) + } + + public func testRemoveLiquidity() { + let completionExpectation = XCTestExpectation(description: "Completion called") + + let deadline = Date().addingTimeInterval(24 * 60 * 60) // 24 hours in the future + exchangeClient.withdrawLiquidity( + from: Wallet.testWallet.address, + signatureProvider: Wallet.testWallet, + liquidityBurned: 100, + tezToWidthdraw: Tez(0.000_001), + minTokensToWithdraw: 1, + deadline: deadline + ) { result in + switch result { + case .failure(let error): + print(error) + XCTFail() + case .success(let hash): + print(hash) + completionExpectation.fulfill() + } + } + + wait(for: [ completionExpectation ], timeout: .expectationTimeout) + } + + public func testTradeTezForToken() { + let completionExpectation = XCTestExpectation(description: "Completion called") + + let deadline = Date().addingTimeInterval(24 * 60 * 60) // 24 hours in the future + + exchangeClient.tradeTezForToken( + source: Wallet.testWallet.address, + amount: Tez(10.0), + signatureProvider: Wallet.testWallet, + minTokensToPurchase: 1, + deadline: deadline + ) { result in + switch result { + case .failure(let error): + print(error) + XCTFail() + case .success(let hash): + print(hash) + completionExpectation.fulfill() + } + } + + wait(for: [ completionExpectation ], timeout: .expectationTimeout) + } + + func testTradeTokenForTez() { + let completionExpectation = XCTestExpectation(description: "Completion called") + + let deadline = Date().addingTimeInterval(24 * 60 * 60) // 24 hours in the future + + exchangeClient.tradeTokenForTez( + source: Wallet.testWallet.address, + signatureProvider: Wallet.testWallet, + tokensToSell: 1, + minTezToBuy: Tez(0.000_001), + deadline: deadline + ) { result in + switch result { + case .failure(let error): + print(error) + XCTFail() + case .success(let hash): + print(hash) + completionExpectation.fulfill() + } + } + + wait(for: [ completionExpectation ], timeout: .expectationTimeout) + } +} diff --git a/IntegrationTests/Dexter/TokenContractIntegrationTests.swift b/IntegrationTests/Dexter/TokenContractIntegrationTests.swift index 59f0d2d5..d565369f 100644 --- a/IntegrationTests/Dexter/TokenContractIntegrationTests.swift +++ b/IntegrationTests/Dexter/TokenContractIntegrationTests.swift @@ -3,7 +3,7 @@ @testable import TezosKit import XCTest -/// Integration tests to run against a Dexter Token Contract. These tests require a live alphanet node. +/// Integration tests to run against a DEXter Token Contract. These tests require a live alphanet node. /// /// To get an alphanet node running locally, follow instructions here: /// https://tezos.gitlab.io/alphanet/introduction/howtoget.html @@ -14,7 +14,8 @@ import XCTest /// /// Before running the tests, you should make sure that there's sufficient tokens in the owners account (which is /// tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW) in the token contract at: -/// https://alphanet.tzscan.io/KT1PARMPddZ9WD1MPmPthXYBCgErmxAHKBD8 +/// Token Contract: https://alphanet.tzscan.io/KT1PARMPddZ9WD1MPmPthXYBCgErmxAHKBD8 +/// Address: https://alphanet.tzscan.io/tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW extension Address { public static let tokenContractAddress = "KT1WiDkoaKgH6dcmHa3tLJKzfnW5QuPjppgn" diff --git a/Tests/Dexter/DexterExchangeClientTests.swift b/Tests/Dexter/DexterExchangeClientTests.swift new file mode 100644 index 00000000..c35127f7 --- /dev/null +++ b/Tests/Dexter/DexterExchangeClientTests.swift @@ -0,0 +1,152 @@ +// Copyright Keefer Taylor, 2019. + +@testable import TezosKit +import XCTest + +final class DexterExchangeClientTests: XCTestCase { + private var exchangeClient: DexterExchangeClient? + + override func setUp() { + super.setUp() + + let contract = Address.testExchangeContractAddress + let networkClient = FakeNetworkClient.tezosNodeNetworkClient + + let tezosNodeClient = TezosNodeClient(networkClient: networkClient) + exchangeClient = DexterExchangeClient( + exchangeContractAddress: contract, + tezosNodeClient: tezosNodeClient + ) + } + + func testGetExchangeLiquidity() { + let expectation = XCTestExpectation(description: "completion called") + + exchangeClient?.getExchangeLiquidity { result in + switch result { + case .success: + expectation.fulfill() + case .failure: + XCTFail() + } + } + + wait(for: [expectation], timeout: .expectationTimeout) + } + + func testGetExchangeBalanceTokens() { + let expectation = XCTestExpectation(description: "completion called") + + exchangeClient?.getExchangeBalanceTokens(tokenContractAddress: .testTokenContractAddress) { result in + switch result { + case .success: + expectation.fulfill() + case .failure: + XCTFail() + } + } + + wait(for: [expectation], timeout: .expectationTimeout) + } + + func testGetExchangeBalanceTez() { + let expectation = XCTestExpectation(description: "completion called") + + exchangeClient?.getExchangeBalanceTez { result in + switch result { + case .success: + expectation.fulfill() + case .failure: + XCTFail() + } + } + + wait(for: [expectation], timeout: .expectationTimeout) + } + + func testAddLiquidity() { + let expectation = XCTestExpectation(description: "completion called") + + exchangeClient?.addLiquidity( + from: Address.testAddress, + amount: Tez(1.0), + signatureProvider: FakeSignatureProvider.testSignatureProvider, + minLiquidity: 1, + maxTokensDeposited: 1, + deadline: Date() + ) { result in + switch result { + case .success: + expectation.fulfill() + case .failure: + XCTFail() + } + } + + wait(for: [expectation], timeout: .expectationTimeout) + } + + func testWithdrawLiquidity() { + let expectation = XCTestExpectation(description: "completion called") + + exchangeClient?.withdrawLiquidity( + from: Address.testAddress, + signatureProvider: FakeSignatureProvider.testSignatureProvider, + liquidityBurned: 1, + tezToWidthdraw: Tez(1.0), + minTokensToWithdraw: 1, + deadline: Date() + ) { result in + switch result { + case .success: + expectation.fulfill() + case .failure: + XCTFail() + } + } + + wait(for: [expectation], timeout: .expectationTimeout) + } + + func testTradeTezToTokens() { + let expectation = XCTestExpectation(description: "completion called") + + exchangeClient?.tradeTezForToken( + source: .testAddress, + amount: Tez(1.0), + signatureProvider: FakeSignatureProvider.testSignatureProvider, + minTokensToPurchase: 1, + deadline: Date() + ) { result in + switch result { + case .success: + expectation.fulfill() + case .failure: + XCTFail() + } + } + + wait(for: [expectation], timeout: .expectationTimeout) + } + + func testTradeTokensForTez() { + let expectation = XCTestExpectation(description: "completion called") + + exchangeClient?.tradeTokenForTez( + source: .testAddress, + signatureProvider: FakeSignatureProvider.testSignatureProvider, + tokensToSell: 1, + minTezToBuy: Tez(1.0), + deadline: Date() + ) { result in + switch result { + case .success: + expectation.fulfill() + case .failure: + XCTFail() + } + } + + wait(for: [expectation], timeout: .expectationTimeout) + } +} diff --git a/Tests/TestObjects.swift b/Tests/TestObjects.swift index f26990d6..128ecf80 100644 --- a/Tests/TestObjects.swift +++ b/Tests/TestObjects.swift @@ -13,6 +13,7 @@ extension String { public static let testSignature = "edsigabc123" public static let testAddress = "tz1abc123xyz" public static let testTokenContractAddress = "tz1tokencontract" + public static let testExchangeContractAddress = "tz1exchangecontract" public static let testDestinationAddress = "tz1destination" public static let testForgeResult = "test_forge_result" public static let testPublicKey = "edpk_test" @@ -167,7 +168,9 @@ extension FakeNetworkClient { "/chains/main/blocks/" + .testBranch + "/helpers/preapply/operations": "[{\"contents\":[{\"kind\":\"transaction\",\"source\":\"tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW\",\"fee\":\"1272\",\"counter\":\"30801\",\"gas_limit\":\"10100\",\"storage_limit\":\"257\",\"amount\":\"1\",\"destination\":\"tz3WXYtyDUNL91qfiCJtVUX746QpNv5i5ve5\",\"metadata\":{\"balance_updates\":[{\"kind\":\"contract\",\"contract\":\"tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW\",\"change\":\"-1272\"},{\"kind\":\"freezer\",\"category\":\"fees\",\"delegate\":\"tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU\",\"level\":125,\"change\":\"1272\"}],\"operation_result\":{\"status\":\"applied\",\"balance_updates\":[{\"kind\":\"contract\",\"contract\":\"tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW\",\"change\":\"-1\"},{\"kind\":\"contract\",\"contract\":\"tz3WXYtyDUNL91qfiCJtVUX746QpNv5i5ve5\",\"change\":\"1\"}],\"consumed_gas\":\"10100\"}}}],\"signature\":\"edsigtpsh2VpWyZTZ46q9j54VfsWZLZuxL7UGEhfgCNx6SXwaWu4gMHx59bRdogbSmDCCpXeQeighgpHk5x32k3rtFu8w5EZyEr\"}]\n", "/chains/main/blocks/head/helpers/scripts/run_operation": "{\"contents\":[{\"kind\":\"origination\",\"source\":\"tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW\",\"fee\":\"1265\",\"counter\":\"31038\",\"gas_limit\":\"10000\",\"storage_limit\":\"257\",\"manager_pubkey\":\"tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW\",\"balance\":\"0\",\"metadata\":{\"balance_updates\":[{\"kind\":\"contract\",\"contract\":\"tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW\",\"change\":\"-1265\"},{\"kind\":\"freezer\",\"category\":\"fees\",\"delegate\":\"tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU\",\"cycle\":247,\"change\":\"1265\"}],\"operation_result\":{\"status\":\"applied\",\"balance_updates\":[{\"kind\":\"contract\",\"contract\":\"tz1XVJ8bZUXs7r5NV8dHvuiBhzECvLRLR3jW\",\"change\":\"-257000\"}],\"originated_contracts\":[\"KT1RAHAXehUNusndqZpcxM8SfCjLi83utZsR\"],\"consumed_gas\":\"10000\"}}}]}\n", "/injection/operation": "\"ooTransactionHash\"", - "/chains/main/blocks/head/context/contracts/tz1tokencontract/big_map_get": "{\"args\":[{\"int\":\"999\"},[]],\"prim\":\"Pair\"}" + "/chains/main/blocks/head/context/contracts/tz1tokencontract/big_map_get": "{\"args\":[{\"int\":\"999\"},[]],\"prim\":\"Pair\"}", + "/chains/main/blocks/head/context/contracts/tz1exchangecontract/storage": "{\"prim\":\"Pair\",\"args\":[[],{\"prim\":\"Pair\",\"args\":[{\"prim\":\"Pair\",\"args\":[{\"string\":\"KT1VsiG5djAjLqZcjEpXBxWEv1ocuW178Psa\"},{\"string\":\"KT1WiDkoaKgH6dcmHa3tLJKzfnW5QuPjppgn\"}]},{\"prim\":\"Pair\",\"args\":[{\"int\":\"1089999900\"},[]]}]}]}", + "/chains/main/blocks/head/context/contracts/tz1exchangecontract/balance": "\"100\"" ] public static let tezosNodeNetworkClient = diff --git a/FeeEstimatorTest.swift b/Tests/TezosKit/FeeEstimatorTest.swift similarity index 100% rename from FeeEstimatorTest.swift rename to Tests/TezosKit/FeeEstimatorTest.swift diff --git a/Tests/TezosKit/GetContractStorageRPCTest.swift b/Tests/TezosKit/GetContractStorageRPCTest.swift index 8cca4e39..3e721605 100644 --- a/Tests/TezosKit/GetContractStorageRPCTest.swift +++ b/Tests/TezosKit/GetContractStorageRPCTest.swift @@ -8,7 +8,7 @@ final class GetContractStorageRPCTest: XCTestCase { let address = "abc123" let rpc = GetContractStorageRPC(address: address) - XCTAssertEqual(rpc.endpoint, "chains/main/blocks/head/context/contracts/\(address)/storage") + XCTAssertEqual(rpc.endpoint, "/chains/main/blocks/head/context/contracts/\(address)/storage") XCTAssertNil(rpc.payload) XCTAssertFalse(rpc.isPOSTRequest) } diff --git a/Tests/TezosKit/MichelsonTests.swift b/Tests/TezosKit/MichelsonTests.swift index 872d85bc..5936f442 100644 --- a/Tests/TezosKit/MichelsonTests.swift +++ b/Tests/TezosKit/MichelsonTests.swift @@ -46,6 +46,13 @@ final class MichelsonTests: XCTestCase { XCTAssertEqual(encoded, Helpers.orderJSONString(MichelsonTests.expectedMichelsonUnitEncoding)) } + func testEncodeDateToJSON() { + let date = Date(timeIntervalSince1970: 1_593_453_621) // Monday, June 29, 2020 6:00:21 PM, GMT + let michelson = StringMichelsonParameter(date: date) + let encoded = JSONUtils.jsonString(for: michelson.networkRepresentation) + XCTAssertEqual(encoded, Helpers.orderJSONString("{\"string\":\"2020-06-29T18:00:21Z\"}")) + } + func testEncodeStringToJSON() { let michelson = MichelsonTests.michelsonString let encoded = JSONUtils.jsonString(for: michelson.networkRepresentation) diff --git a/TezosKit.xcodeproj/project.pbxproj b/TezosKit.xcodeproj/project.pbxproj index e62142a7..1bb73d27 100644 --- a/TezosKit.xcodeproj/project.pbxproj +++ b/TezosKit.xcodeproj/project.pbxproj @@ -52,6 +52,9 @@ 779A9C79230E2C4D004C6575 /* FeeEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A9C78230E2C4A004C6575 /* FeeEstimator.swift */; }; 779A9C7D230E320C004C6575 /* FeeEstimatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A9C7C230E320B004C6575 /* FeeEstimatorTest.swift */; }; 779A9C80230E3CCF004C6575 /* TokenContractClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A9C7F230E3CCF004C6575 /* TokenContractClientTests.swift */; }; + 779A9C82230E457A004C6575 /* DexterExchangeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A9C81230E457A004C6575 /* DexterExchangeClient.swift */; }; + 779A9C85230E4707004C6575 /* DexterExchangeClientIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A9C84230E4707004C6575 /* DexterExchangeClientIntegrationTests.swift */; }; + 779A9C87230E6606004C6575 /* DexterExchangeClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A9C86230E6606004C6575 /* DexterExchangeClientTests.swift */; }; 77B1EADE222496B500EA4FCE /* TezosNodeClient+Promises.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77B1EADD222496B500EA4FCE /* TezosNodeClient+Promises.swift */; }; 77B1EAED2227342200EA4FCE /* PromiseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77F4D26B221F899800D34B01 /* PromiseKit.framework */; }; 77B1EAEF222736FC00EA4FCE /* PromiseKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7774780E222228E50010BA4D /* PromiseKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -289,6 +292,9 @@ 779A9C78230E2C4A004C6575 /* FeeEstimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeEstimator.swift; sourceTree = ""; }; 779A9C7C230E320B004C6575 /* FeeEstimatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeEstimatorTest.swift; sourceTree = ""; }; 779A9C7F230E3CCF004C6575 /* TokenContractClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenContractClientTests.swift; sourceTree = ""; }; + 779A9C81230E457A004C6575 /* DexterExchangeClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexterExchangeClient.swift; sourceTree = ""; }; + 779A9C84230E4707004C6575 /* DexterExchangeClientIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexterExchangeClientIntegrationTests.swift; sourceTree = ""; }; + 779A9C86230E6606004C6575 /* DexterExchangeClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexterExchangeClientTests.swift; sourceTree = ""; }; 77B1EADD222496B500EA4FCE /* TezosNodeClient+Promises.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TezosNodeClient+Promises.swift"; sourceTree = ""; }; 77B1EAE9222730D600EA4FCE /* TezosNodeIntegrationTests+Promises.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TezosNodeIntegrationTests+Promises.swift"; sourceTree = ""; }; 77B1EAF0222745F600EA4FCE /* RunOperationRPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunOperationRPC.swift; sourceTree = ""; }; @@ -507,6 +513,7 @@ isa = PBXGroup; children = ( 779A9C73230E0824004C6575 /* TokenContractClient.swift */, + 779A9C81230E457A004C6575 /* DexterExchangeClient.swift */, ); path = Dexter; sourceTree = ""; @@ -515,6 +522,7 @@ isa = PBXGroup; children = ( 779A9C76230E09DB004C6575 /* TokenContractIntegrationTests.swift */, + 779A9C84230E4707004C6575 /* DexterExchangeClientIntegrationTests.swift */, ); path = Dexter; sourceTree = ""; @@ -523,6 +531,7 @@ isa = PBXGroup; children = ( 779A9C7F230E3CCF004C6575 /* TokenContractClientTests.swift */, + 779A9C86230E6606004C6575 /* DexterExchangeClientTests.swift */, ); path = Dexter; sourceTree = ""; @@ -586,7 +595,6 @@ 77CE358C21F7DC76006ADABA = { isa = PBXGroup; children = ( - 779A9C7C230E320B004C6575 /* FeeEstimatorTest.swift */, 779427BE22C02AA800559A03 /* TezosCrypto.framework */, 7774780E222228E50010BA4D /* PromiseKit.framework */, 7791E60F21FE862600957650 /* TezosKitExample.playground */, @@ -788,6 +796,7 @@ 77F4D27322221CCE00D34B01 /* TezosKit */ = { isa = PBXGroup; children = ( + 779A9C7C230E320B004C6575 /* FeeEstimatorTest.swift */, 77633D522247EFE20011106A /* TezosNodeClientTests.swift */, 779A9C592308634E004C6575 /* SimulationResultResponseAdapterTest.swift */, 77FE788B22EFFA7100B85B9D /* MichelsonAnnotationTests.swift */, @@ -1074,6 +1083,7 @@ 779A9C77230E09DB004C6575 /* TokenContractIntegrationTests.swift in Sources */, 774C9B2122A2A0F400CEB509 /* TezosNodeIntegrationTests+Promises.swift in Sources */, 77B69ED522534F2B00DB4319 /* ConseilClientIntegrationTests+Promises.swift in Sources */, + 779A9C85230E4707004C6575 /* DexterExchangeClientIntegrationTests.swift in Sources */, 77B69ECF2251127D00DB4319 /* ConseilClientIntegrationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1175,6 +1185,7 @@ 779427B322BB257F00559A03 /* ForgingService.swift in Sources */, 77CE387621FC7ED8006ADABA /* GetCurrentPeriodKindRPC.swift in Sources */, 77CE387921FC7ED8006ADABA /* ForgeOperationRPC.swift in Sources */, + 779A9C82230E457A004C6575 /* DexterExchangeClient.swift in Sources */, 77B69EB9224E5C6500DB4319 /* ConseilQueryRPC.swift in Sources */, 77B1EB06222A102500EA4FCE /* OperationWithCounter.swift in Sources */, 77CE385D21FC7ED8006ADABA /* PeriodKind.swift in Sources */, @@ -1231,6 +1242,7 @@ 77B69EC522510D5400DB4319 /* ConseilEntityTest.swift in Sources */, 77F4D268221CD44100D34B01 /* NetworkClientTest.swift in Sources */, 7776493B227616B200451DD5 /* FakeObjects.swift in Sources */, + 779A9C87230E6606004C6575 /* DexterExchangeClientTests.swift in Sources */, 779427AF22BB1D6700559A03 /* OperationFactoryTest.swift in Sources */, 7719266022DA1E1500E63DE4 /* SimulationServiceTest.swift in Sources */, 77CE36E521F7F49F006ADABA /* RPCTest.swift in Sources */, diff --git a/TezosKit/Dexter/DexterExchangeClient.swift b/TezosKit/Dexter/DexterExchangeClient.swift new file mode 100644 index 00000000..8911ca0a --- /dev/null +++ b/TezosKit/Dexter/DexterExchangeClient.swift @@ -0,0 +1,246 @@ +// Copyright Keefer Taylor, 2019. + +private enum JSON { + public enum Keys { + public static let args = "args" + public static let int = "int" + } +} + +/// A client for a DEXter exchange. +/// - See: https://gitlab.com/camlcase-dev/dexter +public class DexterExchangeClient { + /// An underlying gateway to the Tezos Network. + private let tezosNodeClient: TezosNodeClient + + /// The address of a DEXter exchange contract. + private let exchangeContractAddress: Address + + // MARK: - Balance Queries + + /// Initialize a new DEXter client. + /// + /// - Parameters: + /// - exchangeContractAddress: The address of the exchange contract. + /// - tezosNodeClient: A TezosNodeClient which will make requests to the Tezos Network. Defaults to the default + /// client. + public init(exchangeContractAddress: Address, tezosNodeClient: TezosNodeClient = TezosNodeClient()) { + self.tezosNodeClient = tezosNodeClient + self.exchangeContractAddress = exchangeContractAddress + } + + /// Get the total balance of the exchange in Tez. + public func getExchangeBalanceTez(completion: @escaping (Result) -> Void) { + tezosNodeClient.getBalance(address: exchangeContractAddress, completion: completion) + } + + /// Get the total balance of the exchange in tokens. + public func getExchangeBalanceTokens( + tokenContractAddress: Address, + completion: @escaping(Result) -> Void + ) { + let tokenClient = TokenContractClient(tokenContractAddress: tokenContractAddress, tezosNodeClient: tezosNodeClient) + tokenClient.getTokenBalance(address: exchangeContractAddress, completion: completion) + } + + /// Get the total exchange liquidity. + public func getExchangeLiquidity(completion: @escaping (Result) -> Void) { + tezosNodeClient.getContractStorage(address: exchangeContractAddress) { result in + guard + case let .success(json) = result, + let args0 = json[JSON.Keys.args] as? [Any], + let right0 = args0[1] as? [String: Any], + let args1 = right0[JSON.Keys.args] as? [Any], + let right1 = args1[1] as? [String: Any], + let args2 = right1[JSON.Keys.args] as? [Any], + let left2 = args2[0] as? [String: Any], + let balanceString = left2[JSON.Keys.int] as? String, + let balance = Int(balanceString) + else { + completion(result.map { _ in 0 }) + return + } + + completion(.success(balance)) + } + } + + // MARK: - Liquidity Management + + /// Add liquidity to the exchange. + /// + /// - Parameters: + /// - source: The address adding the liquidity + /// - amount: The amount of liquidity to add. + /// - signatureProvider: An opaque object that can sign the operation. + /// - minLiquidity: The minimum liquidity the address is willing to accept. + /// - maxTokens: The maximum amount of tokens the address is willing to add to the liquidity pool. + /// - deadline: A deadline for the transaction to occur by. + /// - completion: A completion block which will be called with the result hash, if successful. + public func addLiquidity( + from source: Address, + amount: Tez, + signatureProvider: SignatureProvider, + minLiquidity: Int, + maxTokensDeposited: Int, + deadline: Date, + completion: @escaping (Result) -> Void + ) { + let parameter = LeftMichelsonParameter( + arg: LeftMichelsonParameter( + arg: PairMichelsonParameter( + left: IntMichelsonParameter(int: minLiquidity), + right: PairMichelsonParameter( + left: IntMichelsonParameter(int: maxTokensDeposited), + right: StringMichelsonParameter(date: deadline) + ) + ) + ) + ) + + tezosNodeClient.call( + contract: exchangeContractAddress, + amount: amount, + parameter: parameter, + source: source, + signatureProvider: signatureProvider, + operationFeePolicy: .estimate, + completion: completion + ) + } + + /// Withdraw liquidity from the exchange. + /// + /// - Parameters: + /// - source: The address adding the liquidity + /// - signatureProvider: An opaque object that can sign the operation. + /// - liquidityBurned: The amount of liquidity to remove from the exchange. + /// - tezToWithdraw: The amount of Tez to withdraw from the exchange. + /// - minTokensToWithdraw: The minimum number of tokens to withdraw. + /// - deadline: A deadline for the transaction to occur by. + /// - completion: A completion block which will be called with the result hash, if successful. + public func withdrawLiquidity( + from source: Address, + signatureProvider: SignatureProvider, + liquidityBurned: Int, + tezToWidthdraw: Tez, + minTokensToWithdraw: Int, + deadline: Date, + completion: @escaping (Result) -> Void + ) { + guard let mutezToWithdraw = Int(tezToWidthdraw.rpcRepresentation) else { + completion(.failure(TezosKitError(kind: .unknown))) + return + } + + let parameter = LeftMichelsonParameter( + arg: RightMichelsonParameter( + arg: PairMichelsonParameter( + left: PairMichelsonParameter( + left: IntMichelsonParameter(int: liquidityBurned), + right: IntMichelsonParameter(int: mutezToWithdraw) + ), + right: PairMichelsonParameter( + left: IntMichelsonParameter(int: minTokensToWithdraw), + right: StringMichelsonParameter(date: deadline) + ) + ) + ) + ) + + tezosNodeClient.call( + contract: exchangeContractAddress, + amount: Tez.zeroBalance, + parameter: parameter, + source: source, + signatureProvider: signatureProvider, + operationFeePolicy: .estimate, + completion: completion + ) + } + + // MARK: - Trades + + /// Buy tokens with Tez. + /// + /// - Parameters: + /// - source: The address making the trade. + /// - amount: The amount of Tez to sell. + /// - signatureProvider: An opaque object that can sign the transaction. + /// - minTokensToPurchase: The minimum number of tokens to purchase. + /// - deadline: A deadline for the transaction to occur by. + /// - completion: A completion block which will be called with the result hash, if successful. + public func tradeTezForToken( + source: Address, + amount: Tez, + signatureProvider: SignatureProvider, + minTokensToPurchase: Int, + deadline: Date, + completion: @escaping (Result) -> Void + ) { + let parameter = RightMichelsonParameter( + arg: LeftMichelsonParameter( + arg: PairMichelsonParameter( + left: IntMichelsonParameter(int: minTokensToPurchase), + right: StringMichelsonParameter(date: deadline) + ) + ) + ) + + tezosNodeClient.call( + contract: exchangeContractAddress, + amount: amount, + parameter: parameter, + source: source, + signatureProvider: signatureProvider, + operationFeePolicy: .estimate, + completion: completion + ) + } + + /// Buy Tez with tokens. + /// + /// - Parameters: + /// - source: The address making the trade. + /// - signatureProvider: An opaque object that can sign the transaction. + /// - tokensToSell: The number of tokens to sell. + /// - minTezToBuy: The minimum number of Tez to buy. + /// - deadline: A deadline for the transaction to occur by. + /// - completion: A completion block which will be called with the result hash, if successful. + public func tradeTokenForTez( + source: Address, + signatureProvider: SignatureProvider, + tokensToSell: Int, + minTezToBuy: Tez, + deadline: Date, + completion: @escaping (Result) -> Void + ) { + guard let minMutezToBuy = Int(minTezToBuy.rpcRepresentation) else { + completion(.failure(TezosKitError(kind: .unknown))) + return + } + + let parameter = RightMichelsonParameter( + arg: RightMichelsonParameter( + arg: LeftMichelsonParameter( + arg: PairMichelsonParameter( + left: IntMichelsonParameter(int: tokensToSell), + right: PairMichelsonParameter( + left: IntMichelsonParameter(int: minMutezToBuy), + right: StringMichelsonParameter(date: deadline) + ) + ) + ) + ) + ) + + tezosNodeClient.call( + contract: exchangeContractAddress, + parameter: parameter, + source: source, + signatureProvider: signatureProvider, + operationFeePolicy: .estimate, + completion: completion + ) + } +} diff --git a/TezosKit/Dexter/TokenContractClient.swift b/TezosKit/Dexter/TokenContractClient.swift index 6aedb4ea..5ee96820 100644 --- a/TezosKit/Dexter/TokenContractClient.swift +++ b/TezosKit/Dexter/TokenContractClient.swift @@ -26,7 +26,7 @@ public class TokenContractClient { public init( tokenContractAddress: Address, tezosNodeClient: TezosNodeClient = TezosNodeClient() - ) { + ) { self.tezosNodeClient = tezosNodeClient self.tokenContractAddress = tokenContractAddress } @@ -70,6 +70,7 @@ public class TokenContractClient { ) } + /// Retrieve the token balance for the given address. public func getTokenBalance(address: Address, completion: @escaping (Result) -> Void) { let key = StringMichelsonParameter(string: address) tezosNodeClient.getBigMapValue(address: tokenContractAddress, key: key, type: .address) { result in diff --git a/TezosKit/Michelson/StringMichelsonParameter.swift b/TezosKit/Michelson/StringMichelsonParameter.swift index dcd7516d..ab9797e9 100644 --- a/TezosKit/Michelson/StringMichelsonParameter.swift +++ b/TezosKit/Michelson/StringMichelsonParameter.swift @@ -7,4 +7,14 @@ public class StringMichelsonParameter: AbstractMichelsonParameter { public init(string: String, annotations: [MichelsonAnnotation]? = nil) { super.init(networkRepresentation: [MichelineConstants.string: string], annotations: annotations) } + + public convenience init(date: Date, annotations: [MichelsonAnnotation]? = nil) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + dateFormatter.timeZone = TimeZone(abbreviation: "GMT") + + let string = dateFormatter.string(from: date) + + self.init(string: string, annotations: annotations) + } } diff --git a/TezosKit/RPC/GetContractStorageRPC.swift b/TezosKit/RPC/GetContractStorageRPC.swift index bcca2d38..c83fdd4b 100644 --- a/TezosKit/RPC/GetContractStorageRPC.swift +++ b/TezosKit/RPC/GetContractStorageRPC.swift @@ -6,7 +6,7 @@ import Foundation public class GetContractStorageRPC: RPC<[String: Any]> { /// - Parameter address: The address to retrieve info about. public init(address: Address) { - let endpoint = "chains/main/blocks/head/context/contracts/\(address)/storage" + let endpoint = "/chains/main/blocks/head/context/contracts/\(address)/storage" super.init(endpoint: endpoint, responseAdapterClass: JSONDictionaryResponseAdapter.self) } }