Jesulonimi21 / Swift-Algorand-Sdk

A Swift Sdk for interacting with the Algorand Blockchain
MIT License
19 stars 13 forks source link

Feature Request: Creating a wrapper to expose the client methods using `async await` #27

Open Vilijan opened 2 years ago

Vilijan commented 2 years ago

Using the latest Swift's practices for Concurrency will make the code that is using the Swift-Algorand-SDK a lot more readable. The idea would be to create a wrapper on top of the current methods that use the async await syntax instead of competition handlers.

A sample example implementation of some of the most common methods:

func suggestedParameters() async throws -> TransactionParametersResponse {
        return try await withCheckedThrowingContinuation({ continuation in
            DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
                self.client.transactionParams().execute(){ paramResponse in
                    if(!(paramResponse.isSuccessful)){
                        continuation.resume(throwing: GeneralError.error)
                    } else {
                        continuation.resume(returning: paramResponse.data!)
                    }
                }
            }
        })
    }

 func waitForConfirmation(transactionID: String) async throws -> String {
        return try await withCheckedThrowingContinuation({ continuation in
            DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
                self.client.pendingTransactionInformation(txId: transactionID).execute() { response in
                    if(response.isSuccessful){
                        continuation.resume(returning: transactionID)
                    }else{
                        continuation.resume(throwing: GeneralError.error)
                    }
                }
            }
        })
 }

 func executeTransaction(encodedTransaction: [Int8]) async throws -> String {
        let transactionID: String = try await withCheckedThrowingContinuation({ continuation in
            DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
                self.client.rawTransaction(rawtxn: encodedTransaction).execute(){ response in
                    if(response.isSuccessful){
                        continuation.resume(returning: response.data!.txId)
                    }else{
                        continuation.resume(throwing: GeneralError.error)
                    }

                }
            }
        })

        return try await waitForConfirmation(transactionID: transactionID)
   }

Then we can use those methods, which makes the code a lot more readable. Sample of creating new asset:

let account = try! Account()

let suggestedParams = try await self.suggestedParameters()

let create_asa_txn = try! Transaction.assetCreateTransactionBuilder()
              .setSender(sdk_account.address)
              .setAssetTotal(assetTotal: 1)
              .setAssetDecimals(assetDecimals:  0)
              .assetUnitName(assetUnitName: "APPL")
              .assetName(assetName:  "Swift SDK")
              .manager(manager: sdk_account.address)
              .reserve(reserve: sdk_account.address)
              .defaultFrozen(defaultFrozen:  false)
              .suggestedParams(params: suggestedParams)
              .build()

 let signedTransaction = account.signTransaction(tx: create_asa_txn)

 let transactionID =  try await self.executeTransaction(encodedTransaction: CustomEncoder.encodeToMsgPack(signedTransaction))
stefanomondino commented 2 years ago

Hi @Vilijan this seems like a reasonable request and I can definetly take a look at it, but I'm a little bit concerned about the supported min iOS version (currently targeting iOS 10.0 with cocoapods) while Swift Concurrency is only available starting from iOS 13.0. I think there's a workaround using #if canImport but i just want to make sure this doesn't introduce any breaking change to other users.

I'll dig into it asap :)

stefanomondino commented 2 years ago

hi @Vilijan, sorry for the delay. Turns out this could be quite simple i guess, with something like this (work in progress)

public extension Request {
    func execute() async throws -> Response<ResponseType> {
        try await withCheckedThrowingContinuation({ continuation in
            self.execute {
                continuation.resume(returning: $0)
            }
        })
    }
}

In this way every client operation could be easily used like this:

let suggestedParams = try await self.client.transactionParams().execute()

what do you think? Can you give it a shot in your project? Also I'd agree with you in having functions throwing errors, but doing that would require us to review all current method signatures introducing lots of breaking changes and I honestly don't know if it's worth it

stefanomondino commented 2 years ago

(Btw: I've using a throwing function in my example anyway so that at least the async implementation is future proof)