diff --git a/KinBase.podspec b/KinBase.podspec index 05a9077..d00825c 100644 --- a/KinBase.podspec +++ b/KinBase.podspec @@ -8,7 +8,7 @@ # Pod::Spec.new do |s| s.name = 'KinBase' - s.version = '2.0.0' + s.version = '2.1.0' s.summary = 'Kin SDK for iOS' s.description = <<-DESC diff --git a/KinBase/KinBase.xcodeproj/project.pbxproj b/KinBase/KinBase.xcodeproj/project.pbxproj index d9c60cc..8f7ae1e 100644 --- a/KinBase/KinBase.xcodeproj/project.pbxproj +++ b/KinBase/KinBase.xcodeproj/project.pbxproj @@ -1799,7 +1799,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "$(inherited)", "-l\"c++\"", @@ -1840,7 +1840,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/KinBase/KinBase/Src/KinAccountContext.swift b/KinBase/KinBase/Src/KinAccountContext.swift index 74f1095..ad2c00a 100644 --- a/KinBase/KinBase/Src/KinAccountContext.swift +++ b/KinBase/KinBase/Src/KinAccountContext.swift @@ -425,7 +425,7 @@ extension KinAccountContext: KinPaymentWriteOperations { var attemptNumber = 0 let invalidAccountErrorRetryStrategy = BackoffStrategy.fixed(after: 3, maxAttempts: MAX_ATTEMPTS) - func buildAttempt() -> Promise { + func buildAttempt(error: KinServiceV4.Errors? = nil) -> Promise { all(self.getAccount(), self.getFee()) .then(on: self.dispatchQueue) { account, fee -> Promise in var sourceAccountPromise: Promise @@ -453,6 +453,9 @@ extension KinAccountContext: KinPaymentWriteOperations { } var paymentItemsPromise: Promise<[KinPaymentItem]> + + var createAccountInstructions = [Instruction]() + var additionalSigners = [KeyPair]() if attemptNumber == 0 || destinationAccountSpec == .exact { paymentItemsPromise = Promise { payments } @@ -461,7 +464,16 @@ extension KinAccountContext: KinPaymentWriteOperations { payments.map { paymentItem in self.service.resolveTokenAccounts(account: paymentItem.destAccount) .then { - paymentItem.copy(destAccount: $0.first?.publicKey) + if $0.isEmpty && error == KinServiceV4.Errors.invalidAccount { + return self.service.createTokenAccountForDestination(account: paymentItem.destAccount) + .then { (instructions: [Instruction], newKeypair: KeyPair) in + createAccountInstructions.append(contentsOf: instructions) + additionalSigners.append(newKeypair) + return Promise(paymentItem.copy(destAccount: newKeypair.asPublicKey())) + } + } else { + return Promise(paymentItem.copy(destAccount: $0.first?.publicKey)) + } } .recover { _ in Promise { paymentItem } } } @@ -478,7 +490,9 @@ extension KinAccountContext: KinPaymentWriteOperations { nonce: accountData.nonce, paymentItems: it, memo: memo, - fee: fee // feeOverride ?: fee + fee: fee, + createAccountInstructions: createAccountInstructions, + additionalSigners: additionalSigners ) } } @@ -486,9 +500,9 @@ extension KinAccountContext: KinPaymentWriteOperations { } } - func attempt() -> Promise<[KinPayment]> { + func attempt(error: KinServiceV4.Errors? = nil) -> Promise<[KinPayment]> { func buildSignSubmit() -> Promise { - return buildAttempt().then { signedTransaction -> Promise in + return buildAttempt(error: error).then { signedTransaction -> Promise in guard let transaction: KinTransaction = try? KinTransaction(envelopeXdrBytes: signedTransaction.envelopeXdrBytes, record: signedTransaction.record, network: signedTransaction.network, invoiceList: invoiceList) else { return Promise(Errors.unknown) } @@ -530,11 +544,10 @@ extension KinAccountContext: KinPaymentWriteOperations { } if (error as? KinServiceV4.Errors) == KinServiceV4.Errors.badSequenceNumber { self.service.invalidateRecentBlockHashCache() - return attempt() - - } else if (error as? KinServiceV4.Errors == KinServiceV4.Errors.invalidAccount) { + return attempt(error: error as? KinServiceV4.Errors) + } else if (error as? KinServiceV4.Errors) == KinServiceV4.Errors.invalidAccount { Thread.sleep(until: Date().addingTimeInterval((try? invalidAccountErrorRetryStrategy.nextDelay()) ?? 0)) - return attempt() + return attempt(error: error as? KinServiceV4.Errors) } else { return Promise.init(error) } diff --git a/KinBase/KinBase/Src/Network/KinService.swift b/KinBase/KinBase/Src/Network/KinService.swift index 8d45698..3420b43 100644 --- a/KinBase/KinBase/Src/Network/KinService.swift +++ b/KinBase/KinBase/Src/Network/KinService.swift @@ -19,6 +19,8 @@ public protocol KinServiceType { func createAccount(account: PublicKey, signer: KeyPair, appIndex: AppIndex?) -> Promise + func createTokenAccountForDestination(account: PublicKey) -> Promise<([Instruction], KeyPair)> + func getAccount(account: PublicKey) -> Promise func resolveTokenAccounts(account: PublicKey) -> Promise<[AccountDescription]> @@ -35,7 +37,7 @@ public protocol KinServiceType { func canWhitelistTransactions() -> Promise - func buildAndSignTransaction(ownerKey: KeyPair, sourceKey: PublicKey, nonce: Int64, paymentItems: [KinPaymentItem], memo: KinMemo, fee: Quark) -> Promise + func buildAndSignTransaction(ownerKey: KeyPair, sourceKey: PublicKey, nonce: Int64, paymentItems: [KinPaymentItem], memo: KinMemo, fee: Quark, createAccountInstructions: [Instruction], additionalSigners: [KeyPair]) -> Promise func submitTransaction(transaction: KinTransaction) -> Promise @@ -442,6 +444,59 @@ extension KinServiceV4 : KinServiceType { } } } + + public func createTokenAccountForDestination(account: PublicKey) -> Promise<([Instruction], KeyPair)> { + networkOperationHandler.queueWork { [weak self] respond in + guard let self = self else { + respond.onError?(Errors.unknown) + return + } + + all(self.cachedServiceConfig(), self.cachedMinRentExemption()).then { serviceConfig, minRentExemption in + guard + serviceConfig.result == GetServiceConfigResponseV4.Result.ok || + minRentExemption.result == GetMinimumBalanceForRentExemptionResponseV4.Result.ok + else { + respond.onError?(Errors.unknown) + return + } + + let subsidizer = serviceConfig.subsidizerAccount! + let programKey = serviceConfig.tokenProgram! + let mint = serviceConfig.token! + + guard let ephemeralKeypair = KeyPair.generate() + else { + respond.onError?(Errors.unknown) + return + } + let pub = ephemeralKeypair.asPublicKey() + + let instructions: [Instruction] = [ + SystemProgram.createAccountInstruction( + subsidizer: subsidizer, + address: pub, + owner: programKey, + lamports: minRentExemption.lamports, + size: TokenProgram.accountSize + ), + TokenProgram.initializeAccountInstruction( + account: pub, + mint: mint, + owner: pub, + programKey: programKey + ), + TokenProgram.setAuthority(account: pub, currentAuthority: pub, newAuthority: subsidizer, authorityType: .authorityCloseAccount, programKey: programKey), + TokenProgram.setAuthority(account: pub, currentAuthority: pub, newAuthority: account, authorityType: .authorityAccountHolder, programKey: programKey) + ] + + respond.onSuccess((instructions, ephemeralKeypair)) + } + .catch { + respond.onError?($0) + } + } + } public func getAccount(account: PublicKey) -> Promise { return networkOperationHandler.queueWork { [weak self] respond in @@ -641,7 +696,7 @@ extension KinServiceV4 : KinServiceType { return Promise.init(true) } - public func buildAndSignTransaction(ownerKey: KeyPair, sourceKey: PublicKey, nonce: Int64, paymentItems: [KinPaymentItem], memo: KinMemo, fee: Quark) -> Promise { + public func buildAndSignTransaction(ownerKey: KeyPair, sourceKey: PublicKey, nonce: Int64, paymentItems: [KinPaymentItem], memo: KinMemo, fee: Quark, createAccountInstructions: [Instruction], additionalSigners: [KeyPair]) -> Promise { return networkOperationHandler.queueWork { [weak self] respond in guard let self = self else { respond.onError?(Errors.unknown) @@ -680,6 +735,8 @@ extension KinServiceV4 : KinServiceType { ) } } + + instructions.append(contentsOf: createAccountInstructions) instructions.append(contentsOf: paymentItems.map { paymentItem in TokenProgram.transferInstruction( @@ -690,13 +747,15 @@ extension KinServiceV4 : KinServiceType { programKey: programKey ) }) - + + var signers = additionalSigners + signers.insert(signer, at: 0) let transaction = try! Transaction( payer: subsidizer, instructions: instructions ) .updatingBlockhash(recentBlockHash.blockHash!) - .signing(using: signer) + .signing(using: signers) print(transaction) diff --git a/KinBase/KinBase/Src/Solana/Encoding/Transaction.swift b/KinBase/KinBase/Src/Solana/Encoding/Transaction.swift index 7b86fc9..c406708 100644 --- a/KinBase/KinBase/Src/Solana/Encoding/Transaction.swift +++ b/KinBase/KinBase/Src/Solana/Encoding/Transaction.swift @@ -121,6 +121,10 @@ public struct Transaction { // MARK: - Signing - public func signing(using keyPairs: KeyPair...) throws -> Transaction { + // Swift does not support splatting, wrapping the function this way allows it to be called with an array or variadic + return try signing(using: keyPairs) + } + public func signing(using keyPairs: [KeyPair]) throws -> Transaction { let requiredSignatureCount = message.header.signatureCount if keyPairs.count > requiredSignatureCount { throw SigningError.tooManySigners diff --git a/KinBase/KinBaseTests/Network/KinServiceTestsV4.swift b/KinBase/KinBaseTests/Network/KinServiceTestsV4.swift index 04fdfcf..f3fe710 100644 --- a/KinBase/KinBaseTests/Network/KinServiceTestsV4.swift +++ b/KinBase/KinBaseTests/Network/KinServiceTestsV4.swift @@ -275,7 +275,9 @@ class KinServiceTestsV4: XCTestCase { ownerKey: sourceKey, sourceKey: account.publicKey, nonce: account.sequence!, paymentItems: paymentItems, memo: KinMemo(text: "1-kek-blah"), - fee: Quark(100) + fee: Quark(100), + createAccountInstructions: [], + additionalSigners: [] ) .then { transaction in XCTAssertEqual(Data(transaction.envelopeXdrBytes).base64EncodedString(), expectEnvelope) @@ -333,7 +335,9 @@ class KinServiceTestsV4: XCTestCase { ownerKey: sourceKey, sourceKey: account.publicKey, nonce: account.sequence!, paymentItems: paymentItems, memo: agoraMemo.kinMemo, - fee: Quark(100) + fee: Quark(100), + createAccountInstructions: [], + additionalSigners: [] ) .then { transaction in XCTAssertEqual(Data(transaction.envelopeXdrBytes).base64EncodedString(), expectEnvelope) diff --git a/KinBase/KinBaseTests/Tools/MockKinService.swift b/KinBase/KinBaseTests/Tools/MockKinService.swift index d140597..17dbadb 100644 --- a/KinBase/KinBaseTests/Tools/MockKinService.swift +++ b/KinBase/KinBaseTests/Tools/MockKinService.swift @@ -15,6 +15,7 @@ class MockKinService: KinServiceType { var stubGetAccountResult: KinAccount? var stubGetAccountResultPromise: Promise? var stubCreateAccountResult: KinAccount? + var stubCreateTokenAccountForDestinationResult: Promise<([Instruction], KeyPair)>? var stubGetTransactionResult: KinTransaction? var stubBuildAndSignTransactionResult: Promise? var stubSubmitTransactionResult: Promise? @@ -45,6 +46,10 @@ class MockKinService: KinServiceType { return .init(stubCreateAccountResult!) } + func createTokenAccountForDestination(account: PublicKey) -> Promise<([Instruction], KeyPair)> { + return stubCreateTokenAccountForDestinationResult! + } + func streamAccount(account: PublicKey) -> Observable { return stubStreamAccountObservable! } @@ -69,7 +74,7 @@ class MockKinService: KinServiceType { return .init(stubCanWhitelistTransactionResult!) } - func buildAndSignTransaction(ownerKey: KeyPair, sourceKey: PublicKey, nonce: Int64, paymentItems: [KinPaymentItem], memo: KinMemo, fee: Quark) -> Promise { + func buildAndSignTransaction(ownerKey: KeyPair, sourceKey: PublicKey, nonce: Int64, paymentItems: [KinPaymentItem], memo: KinMemo, fee: Quark, createAccountInstructions: [Instruction], additionalSigners: [KeyPair]) -> Promise { return stubBuildAndSignTransactionResult! } diff --git a/KinBase/README.md b/KinBase/README.md index 54e5919..9ecd177 100644 --- a/KinBase/README.md +++ b/KinBase/README.md @@ -7,7 +7,7 @@ The KinBase module is the foundation upon which the rest of the SDK stands. ## Installation Add the following to your Podfile. ``` -pod 'KinBase', '~> 2.0.0' +pod 'KinBase', '~> 2.1.0' ``` ## Overview diff --git a/KinDesign.podspec b/KinDesign.podspec index 05a5f08..148eb7d 100644 --- a/KinDesign.podspec +++ b/KinDesign.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'KinDesign' - s.version = '2.0.0' + s.version = '2.1.0' s.summary = 'Kin Design Library for iOS' s.description = <<-DESC diff --git a/KinDesign/KinDesign.xcodeproj/project.pbxproj b/KinDesign/KinDesign.xcodeproj/project.pbxproj index 8265fdb..c92b4e3 100644 --- a/KinDesign/KinDesign.xcodeproj/project.pbxproj +++ b/KinDesign/KinDesign.xcodeproj/project.pbxproj @@ -430,7 +430,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = org.kin..KinDesign; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -458,7 +458,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = org.kin..KinDesign; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/KinDesign/README.md b/KinDesign/README.md index 5f7f0cf..985f19f 100644 --- a/KinDesign/README.md +++ b/KinDesign/README.md @@ -10,7 +10,7 @@ All of these components can be tested out and browsed in the [sample app](../Kin ## Installation Add the following to your project's Podfile. ``` -pod 'KinDesign', '~> 2.0.0' +pod 'KinDesign', '~> 2.1.0' ``` ### Primary Button diff --git a/KinSampleApp/KinSampleApp.xcodeproj/project.pbxproj b/KinSampleApp/KinSampleApp.xcodeproj/project.pbxproj index 9431160..83528bb 100644 --- a/KinSampleApp/KinSampleApp.xcodeproj/project.pbxproj +++ b/KinSampleApp/KinSampleApp.xcodeproj/project.pbxproj @@ -580,7 +580,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = org.kin..KinSampleApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -602,7 +602,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = org.kin..KinSampleApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/KinUX.podspec b/KinUX.podspec index 6f2af01..586867e 100644 --- a/KinUX.podspec +++ b/KinUX.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'KinUX' - s.version = '2.0.0' + s.version = '2.1.0' s.summary = 'UX Library for Kin SDK iOS' s.description = <<-DESC @@ -20,8 +20,8 @@ Pod::Spec.new do |s| s.source_files = 'KinUX/KinUX/Src/**/*' - s.dependency 'KinBase', '~> 2.0.0' - s.dependency 'KinDesign', '~> 2.0.0' + s.dependency 'KinBase', '~> 2.1.0' + s.dependency 'KinDesign', '~> 2.1.0' # Dependencies needed for KinGrpcApi s.dependency 'gRPC-ProtoRPC' diff --git a/KinUX/KinUX.xcodeproj/project.pbxproj b/KinUX/KinUX.xcodeproj/project.pbxproj index 5380d0c..6bfab49 100644 --- a/KinUX/KinUX.xcodeproj/project.pbxproj +++ b/KinUX/KinUX.xcodeproj/project.pbxproj @@ -589,7 +589,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = org.kin..KinUX; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -619,7 +619,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = org.kin..KinUX; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/KinUX/README.md b/KinUX/README.md index 8569d10..62c885f 100644 --- a/KinUX/README.md +++ b/KinUX/README.md @@ -6,7 +6,7 @@ The KinUX module provides an easy to use out of the box spend flow UI that is pr Add the following to your project's Podfile. This will also transitively pull in KinBase and KinDesign into your project as well. ``` -pod 'KinUX', '~> 2.0.0' +pod 'KinUX', '~> 2.1.0' ``` ## Overview diff --git a/README.md b/README.md index 48b3855..9d71291 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ In your Podfile ``` // If you just want to access the blockchain & no UI -pod 'KinBase', '~> 2.0.0' +pod 'KinBase', '~> 2.1.0' // Add spend to use the modal spend flow to allow users to buy things with Kin -pod 'KinUX', '~> 2.0.0' +pod 'KinUX', '~> 2.1.0' // Add design for direct access to UI views you can use in your own app -pod 'KinDesign', '~> 2.0.0' +pod 'KinDesign', '~> 2.1.0' ``` ## Sample App