From 183a36ce6c0313ef19211427a35790ff749b4ddf Mon Sep 17 00:00:00 2001 From: Tyson Williams Date: Mon, 16 Mar 2026 15:28:13 -0700 Subject: [PATCH 1/2] feat: handle pendingTransactionId and transaction review URLs in signing methods When a partner has permissions policies enabled and a signing operation requires approval, the backend returns pendingTransactionId instead of a signature. This change adds proper handling for this case: - signMessage/signTransaction now return SigningResult enum (.success/.denied) - Add TransactionDeniedResult with pendingTransactionId and transactionReviewUrl - Add setTransactionReviewHandler() for auto-opening review URLs in-app - Update signSolanaSerializedTransaction return type to match Ref: ENG-6540 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ParaSwift/Core/ParaManager+Signing.swift | 143 ++++++++++++------ Sources/ParaSwift/Core/ParaManager.swift | 2 + Sources/ParaSwift/ParaManager+Solana.swift | 6 +- 3 files changed, 98 insertions(+), 53 deletions(-) diff --git a/Sources/ParaSwift/Core/ParaManager+Signing.swift b/Sources/ParaSwift/Core/ParaManager+Signing.swift index 7450c8d..2e95ac3 100644 --- a/Sources/ParaSwift/Core/ParaManager+Signing.swift +++ b/Sources/ParaSwift/Core/ParaManager+Signing.swift @@ -10,19 +10,40 @@ public struct SignatureResult { public let walletId: String /// The wallet type (e.g., "evm", "solana", "cosmos") public let type: String - + public init(signedTransaction: String, walletId: String, type: String) { self.signedTransaction = signedTransaction self.walletId = walletId self.type = type } - + /// Returns the transaction data for broadcasting. public var transactionData: String { return signedTransaction } } +/// Result when a signing operation requires user approval via a permissions policy. +public struct TransactionDeniedResult { + /// The ID of the pending transaction that requires approval. + public let pendingTransactionId: String + /// The URL to open for the user to review and approve the transaction. + public let transactionReviewUrl: String + + public init(pendingTransactionId: String, transactionReviewUrl: String) { + self.pendingTransactionId = pendingTransactionId + self.transactionReviewUrl = transactionReviewUrl + } +} + +/// The result of a signing operation, which can be either a success or a denial requiring approval. +public enum SigningResult { + /// Signing succeeded and the signature is available. + case success(SignatureResult) + /// Signing was denied by a permissions policy and requires user approval. + case denied(TransactionDeniedResult) +} + /// Result of a transfer operation public struct TransferResult { /// The transaction hash @@ -35,7 +56,7 @@ public struct TransferResult { public let amount: String /// The chain ID public let chainId: String - + public init(hash: String, from: String, to: String, amount: String, chainId: String) { self.hash = hash self.from = from @@ -71,24 +92,41 @@ struct FormatAndSignTransactionParams: Encodable { } public extension ParaManager { + /// Sets a global handler for transaction review URLs. When a signing operation + /// requires user approval (e.g., due to permissions policies), the SDK calls this + /// handler with the review URL so you can open it in an in-app browser. + /// + /// - Parameter handler: A closure that receives the review URL string. + /// + /// ```swift + /// paraManager.setTransactionReviewHandler { url in + /// // Open url in ASWebAuthenticationSession or SFSafariViewController + /// } + /// ``` + func setTransactionReviewHandler(_ handler: @escaping (String) -> Void) { + transactionReviewHandler = handler + } + /// Signs a message with any wallet type. /// /// This unified method works with all wallet types (EVM, Solana, Cosmos, etc.). /// The bridge handles the chain-specific signing logic internally. /// + /// When a partner has permissions policies enabled, the backend may return a + /// `pendingTransactionId` instead of a signature. In that case, this method returns + /// `.denied` with the review URL. If a transaction review handler is set, it will + /// be called automatically with the URL. + /// /// - Parameters: /// - walletId: The ID of the wallet to use for signing. /// - message: The message to sign (plain text). - /// - Returns: A SignatureResult containing the signature and metadata. - func signMessage(walletId: String, message: String) async throws -> SignatureResult { + /// - Returns: A `SigningResult` — either `.success` with the signature or `.denied` with the review URL. + func signMessage(walletId: String, message: String) async throws -> SigningResult { try await ensureWebViewReady() - - // Ensure transmission keyshares are loaded before signing - // This is critical for wallet operations to work properly + do { try await ensureTransmissionKeysharesLoaded() } catch { - // Log the error but continue - some wallets may work without transmission keyshares logger.warning("Failed to load transmission keyshares before signing message: \(error.localizedDescription)") } @@ -98,22 +136,30 @@ public extension ParaManager { ) let result = try await postMessage(method: "formatAndSignMessage", payload: params) - - // Parse the response from bridge let dict = try decodeResult(result, expectedType: [String: Any].self, method: "formatAndSignMessage") - - // Message signing returns signature field + + // Check for pending transaction (permissions policy requires approval) + if let pendingTransactionId = dict["pendingTransactionId"] as? String, + let transactionReviewUrl = dict["transactionReviewUrl"] as? String { + let denied = TransactionDeniedResult( + pendingTransactionId: pendingTransactionId, + transactionReviewUrl: transactionReviewUrl + ) + transactionReviewHandler?(transactionReviewUrl) + return .denied(denied) + } + guard let signature = dict["signature"] as? String else { throw ParaError.bridgeError("Missing signature in response") } let returnedWalletId = dict["walletId"] as? String ?? walletId let walletType = dict["type"] as? String ?? "unknown" - - return SignatureResult( - signedTransaction: signature, // Store signature in signedTransaction field for compatibility + + return .success(SignatureResult( + signedTransaction: signature, walletId: returnedWalletId, type: walletType - ) + )) } /// Signs a transaction with any wallet type. @@ -121,35 +167,34 @@ public extension ParaManager { /// This unified method works with all wallet types. It accepts transaction parameters /// as an Encodable object that will be formatted by the bridge based on wallet type. /// + /// When a partner has permissions policies enabled, the backend may return a + /// `pendingTransactionId` instead of a signature. In that case, this method returns + /// `.denied` with the review URL. + /// /// - Parameters: /// - walletId: The ID of the wallet to use for signing. /// - transaction: The transaction parameters (EVMTransaction, SolanaTransaction, etc.). /// - chainId: Optional chain ID (primarily for EVM chains). /// - rpcUrl: Optional RPC URL (required for Solana if recentBlockhash is not provided). - /// - Returns: A SignatureResult containing the signature and metadata. - /// - Note: This method automatically ensures transmission keyshares are loaded before signing. + /// - Returns: A `SigningResult` — either `.success` with the signature or `.denied` with the review URL. func signTransaction( walletId: String, transaction: T, chainId: String? = nil, rpcUrl: String? = nil - ) async throws -> SignatureResult { + ) async throws -> SigningResult { try await ensureWebViewReady() - - // Ensure transmission keyshares are loaded before signing - // This is critical for wallet operations to work properly + do { try await ensureTransmissionKeysharesLoaded() } catch { - // Log the error but continue - some wallets may work without transmission keyshares logger.warning("Failed to load transmission keyshares before signing transaction: \(error.localizedDescription)") } - // Encode the transaction object to JSON let encoder = JSONEncoder() let transactionData = try encoder.encode(transaction) let transactionDict = try JSONSerialization.jsonObject(with: transactionData, options: []) as? [String: Any] ?? [:] - + let params = FormatAndSignTransactionParams( walletId: walletId, transaction: transactionDict, @@ -158,34 +203,39 @@ public extension ParaManager { ) let result = try await postMessage(method: "formatAndSignTransaction", payload: params) - - // Parse the response from bridge let dict = try decodeResult(result, expectedType: [String: Any].self, method: "formatAndSignTransaction") - - // Transaction signing returns signedTransaction (complete tx) or signature (when tx construction isn't possible) + + // Check for pending transaction (permissions policy requires approval) + if let pendingTransactionId = dict["pendingTransactionId"] as? String, + let transactionReviewUrl = dict["transactionReviewUrl"] as? String { + let denied = TransactionDeniedResult( + pendingTransactionId: pendingTransactionId, + transactionReviewUrl: transactionReviewUrl + ) + transactionReviewHandler?(transactionReviewUrl) + return .denied(denied) + } + let signedTransaction: String if let tx = dict["signedTransaction"] as? String { - signedTransaction = tx // Complete transaction ready to broadcast + signedTransaction = tx } else if let sig = dict["signature"] as? String { - signedTransaction = sig // Just signature (pre-serialized Solana, Cosmos fallback) + signedTransaction = sig } else { throw ParaError.bridgeError("Missing signedTransaction or signature in response") } let returnedWalletId = dict["walletId"] as? String ?? walletId let walletType = dict["type"] as? String ?? "unknown" - - return SignatureResult( + + return .success(SignatureResult( signedTransaction: signedTransaction, walletId: returnedWalletId, type: walletType - ) + )) } - + /// Gets the balance for any wallet type. /// - /// This unified method works with all wallet types. For token balances, - /// provide the token contract address or symbol. - /// /// - Parameters: /// - walletId: The ID of the wallet. /// - token: Optional token identifier (contract address for EVM, mint address for Solana, etc.). @@ -215,12 +265,9 @@ public extension ParaManager { let result = try await postMessage(method: "getBalance", payload: params) return try decodeResult(result, expectedType: String.self, method: "getBalance") } - + /// High-level transfer method for EVM chains. /// - /// This method handles ETH/ERC20 transfers on EVM-compatible chains. - /// The bridge handles transaction construction, signing, and broadcasting. - /// /// - Parameters: /// - walletId: The ID of the EVM wallet to transfer from. /// - to: The recipient address. @@ -228,7 +275,6 @@ public extension ParaManager { /// - chainId: Optional chain ID (auto-detected if not provided). /// - rpcUrl: Optional RPC URL (defaults to Ethereum mainnet if not provided). /// - Returns: Transaction result containing hash and details. - /// - Note: This method automatically ensures transmission keyshares are loaded before transferring. func transfer( walletId: String, to: String, @@ -237,13 +283,10 @@ public extension ParaManager { rpcUrl: String? = nil ) async throws -> TransferResult { try await ensureWebViewReady() - - // Ensure transmission keyshares are loaded before transferring - // This is critical for wallet operations to work properly + do { try await ensureTransmissionKeysharesLoaded() } catch { - // Log the error but continue - some wallets may work without transmission keyshares logger.warning("Failed to load transmission keyshares before transfer: \(error.localizedDescription)") } @@ -265,7 +308,7 @@ public extension ParaManager { let result = try await postMessage(method: "transfer", payload: params) let dict = try decodeResult(result, expectedType: [String: Any].self, method: "transfer") - + return TransferResult( hash: dict["hash"] as? String ?? "", from: dict["from"] as? String ?? "", @@ -274,4 +317,4 @@ public extension ParaManager { chainId: dict["chainId"] as? String ?? "" ) } -} \ No newline at end of file +} diff --git a/Sources/ParaSwift/Core/ParaManager.swift b/Sources/ParaSwift/Core/ParaManager.swift index a41c5b3..9db123e 100644 --- a/Sources/ParaSwift/Core/ParaManager.swift +++ b/Sources/ParaSwift/Core/ParaManager.swift @@ -56,6 +56,8 @@ public class ParaManager: NSObject, ObservableObject { internal var transmissionKeysharesLoaded = false /// Default web authentication session used for hosted auth flows. internal var defaultWebAuthenticationSession: WebAuthenticationSession? + /// Handler called when a signing operation requires user approval via a review URL. + internal var transactionReviewHandler: ((String) -> Void)? /// Controller responsible for persisting session snapshots. private var sessionPersistence: SessionPersistenceStoring /// Last serialized session we saved locally to avoid redundant writes. diff --git a/Sources/ParaSwift/ParaManager+Solana.swift b/Sources/ParaSwift/ParaManager+Solana.swift index 002132e..070ebdc 100644 --- a/Sources/ParaSwift/ParaManager+Solana.swift +++ b/Sources/ParaSwift/ParaManager+Solana.swift @@ -23,14 +23,14 @@ public extension ParaManager { /// - Parameters: /// - walletId: The ID of the Solana wallet to use for signing /// - base64Tx: The base64-encoded serialized transaction - /// - Returns: A SignatureResult containing the signature + /// - Returns: A SigningResult — either `.success` with the signature or `.denied` with the review URL. /// - Throws: ParaWebViewError if signing fails func signSolanaSerializedTransaction( walletId: String, base64Tx: String - ) async throws -> SignatureResult { + ) async throws -> SigningResult { let transaction = PreSerializedTransaction(data: base64Tx) - + return try await signTransaction( walletId: walletId, transaction: transaction From e1f16160e8d211a61739cff923602923df1622b4 Mon Sep 17 00:00:00 2001 From: Tyson Williams Date: Mon, 16 Mar 2026 15:34:28 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20simplify=20pendingTransactionId=20ha?= =?UTF-8?q?ndling=20=E2=80=94=20throw=20error=20instead=20of=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the SigningResult enum approach. Instead, signing methods keep their existing SignatureResult return type and throw ParaError.transactionDenied when a permissions policy blocks the operation. No breaking API changes. - Add ParaError.transactionDenied(pendingTransactionId:, transactionReviewUrl:) - Add setTransactionReviewHandler() — auto-opens review URL before throwing - Add checkForTransactionDenial() helper in signing methods Ref: ENG-6540 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ParaSwift/Core/ParaManager+Signing.swift | 153 ++++-------------- Sources/ParaSwift/Core/ParaTypes.swift | 6 + Sources/ParaSwift/ParaManager+Solana.swift | 6 +- 3 files changed, 40 insertions(+), 125 deletions(-) diff --git a/Sources/ParaSwift/Core/ParaManager+Signing.swift b/Sources/ParaSwift/Core/ParaManager+Signing.swift index 2e95ac3..0ca00cb 100644 --- a/Sources/ParaSwift/Core/ParaManager+Signing.swift +++ b/Sources/ParaSwift/Core/ParaManager+Signing.swift @@ -23,27 +23,6 @@ public struct SignatureResult { } } -/// Result when a signing operation requires user approval via a permissions policy. -public struct TransactionDeniedResult { - /// The ID of the pending transaction that requires approval. - public let pendingTransactionId: String - /// The URL to open for the user to review and approve the transaction. - public let transactionReviewUrl: String - - public init(pendingTransactionId: String, transactionReviewUrl: String) { - self.pendingTransactionId = pendingTransactionId - self.transactionReviewUrl = transactionReviewUrl - } -} - -/// The result of a signing operation, which can be either a success or a denial requiring approval. -public enum SigningResult { - /// Signing succeeded and the signature is available. - case success(SignatureResult) - /// Signing was denied by a permissions policy and requires user approval. - case denied(TransactionDeniedResult) -} - /// Result of a transfer operation public struct TransferResult { /// The transaction hash @@ -93,14 +72,12 @@ struct FormatAndSignTransactionParams: Encodable { public extension ParaManager { /// Sets a global handler for transaction review URLs. When a signing operation - /// requires user approval (e.g., due to permissions policies), the SDK calls this - /// handler with the review URL so you can open it in an in-app browser. - /// - /// - Parameter handler: A closure that receives the review URL string. + /// requires user approval (due to permissions policies), the SDK calls this + /// handler with the review URL before throwing ``ParaError/transactionDenied``. /// /// ```swift /// paraManager.setTransactionReviewHandler { url in - /// // Open url in ASWebAuthenticationSession or SFSafariViewController + /// UIApplication.shared.open(URL(string: url)!) /// } /// ``` func setTransactionReviewHandler(_ handler: @escaping (String) -> Void) { @@ -109,19 +86,8 @@ public extension ParaManager { /// Signs a message with any wallet type. /// - /// This unified method works with all wallet types (EVM, Solana, Cosmos, etc.). - /// The bridge handles the chain-specific signing logic internally. - /// - /// When a partner has permissions policies enabled, the backend may return a - /// `pendingTransactionId` instead of a signature. In that case, this method returns - /// `.denied` with the review URL. If a transaction review handler is set, it will - /// be called automatically with the URL. - /// - /// - Parameters: - /// - walletId: The ID of the wallet to use for signing. - /// - message: The message to sign (plain text). - /// - Returns: A `SigningResult` — either `.success` with the signature or `.denied` with the review URL. - func signMessage(walletId: String, message: String) async throws -> SigningResult { + /// - Throws: ``ParaError/transactionDenied`` if a permissions policy requires approval. + func signMessage(walletId: String, message: String) async throws -> SignatureResult { try await ensureWebViewReady() do { @@ -130,59 +96,32 @@ public extension ParaManager { logger.warning("Failed to load transmission keyshares before signing message: \(error.localizedDescription)") } - let params = FormatAndSignMessageParams( - walletId: walletId, - message: message - ) - + let params = FormatAndSignMessageParams(walletId: walletId, message: message) let result = try await postMessage(method: "formatAndSignMessage", payload: params) let dict = try decodeResult(result, expectedType: [String: Any].self, method: "formatAndSignMessage") - // Check for pending transaction (permissions policy requires approval) - if let pendingTransactionId = dict["pendingTransactionId"] as? String, - let transactionReviewUrl = dict["transactionReviewUrl"] as? String { - let denied = TransactionDeniedResult( - pendingTransactionId: pendingTransactionId, - transactionReviewUrl: transactionReviewUrl - ) - transactionReviewHandler?(transactionReviewUrl) - return .denied(denied) - } + try checkForTransactionDenial(dict) guard let signature = dict["signature"] as? String else { throw ParaError.bridgeError("Missing signature in response") } - let returnedWalletId = dict["walletId"] as? String ?? walletId - let walletType = dict["type"] as? String ?? "unknown" - return .success(SignatureResult( + return SignatureResult( signedTransaction: signature, - walletId: returnedWalletId, - type: walletType - )) + walletId: dict["walletId"] as? String ?? walletId, + type: dict["type"] as? String ?? "unknown" + ) } /// Signs a transaction with any wallet type. /// - /// This unified method works with all wallet types. It accepts transaction parameters - /// as an Encodable object that will be formatted by the bridge based on wallet type. - /// - /// When a partner has permissions policies enabled, the backend may return a - /// `pendingTransactionId` instead of a signature. In that case, this method returns - /// `.denied` with the review URL. - /// - /// - Parameters: - /// - walletId: The ID of the wallet to use for signing. - /// - transaction: The transaction parameters (EVMTransaction, SolanaTransaction, etc.). - /// - chainId: Optional chain ID (primarily for EVM chains). - /// - rpcUrl: Optional RPC URL (required for Solana if recentBlockhash is not provided). - /// - Returns: A `SigningResult` — either `.success` with the signature or `.denied` with the review URL. + /// - Throws: ``ParaError/transactionDenied`` if a permissions policy requires approval. func signTransaction( walletId: String, transaction: T, chainId: String? = nil, rpcUrl: String? = nil - ) async throws -> SigningResult { + ) async throws -> SignatureResult { try await ensureWebViewReady() do { @@ -205,16 +144,7 @@ public extension ParaManager { let result = try await postMessage(method: "formatAndSignTransaction", payload: params) let dict = try decodeResult(result, expectedType: [String: Any].self, method: "formatAndSignTransaction") - // Check for pending transaction (permissions policy requires approval) - if let pendingTransactionId = dict["pendingTransactionId"] as? String, - let transactionReviewUrl = dict["transactionReviewUrl"] as? String { - let denied = TransactionDeniedResult( - pendingTransactionId: pendingTransactionId, - transactionReviewUrl: transactionReviewUrl - ) - transactionReviewHandler?(transactionReviewUrl) - return .denied(denied) - } + try checkForTransactionDenial(dict) let signedTransaction: String if let tx = dict["signedTransaction"] as? String { @@ -224,25 +154,15 @@ public extension ParaManager { } else { throw ParaError.bridgeError("Missing signedTransaction or signature in response") } - let returnedWalletId = dict["walletId"] as? String ?? walletId - let walletType = dict["type"] as? String ?? "unknown" - return .success(SignatureResult( + return SignatureResult( signedTransaction: signedTransaction, - walletId: returnedWalletId, - type: walletType - )) + walletId: dict["walletId"] as? String ?? walletId, + type: dict["type"] as? String ?? "unknown" + ) } /// Gets the balance for any wallet type. - /// - /// - Parameters: - /// - walletId: The ID of the wallet. - /// - token: Optional token identifier (contract address for EVM, mint address for Solana, etc.). - /// - rpcUrl: Optional RPC URL (recommended for Solana and Cosmos to avoid 403/CORS issues). - /// - chainPrefix: Optional bech32 prefix for Cosmos (e.g., "juno", "stars"). - /// - denom: Optional denom for Cosmos balances (e.g., "ujuno", "ustars"). - /// - Returns: The balance as a string (format depends on the chain). func getBalance(walletId: String, token: String? = nil, rpcUrl: String? = nil, chainPrefix: String? = nil, denom: String? = nil) async throws -> String { try await ensureWebViewReady() @@ -254,27 +174,12 @@ public extension ParaManager { let denom: String? } - let params = GetBalanceParams( - walletId: walletId, - token: token, - rpcUrl: rpcUrl, - chainPrefix: chainPrefix, - denom: denom - ) - + let params = GetBalanceParams(walletId: walletId, token: token, rpcUrl: rpcUrl, chainPrefix: chainPrefix, denom: denom) let result = try await postMessage(method: "getBalance", payload: params) return try decodeResult(result, expectedType: String.self, method: "getBalance") } /// High-level transfer method for EVM chains. - /// - /// - Parameters: - /// - walletId: The ID of the EVM wallet to transfer from. - /// - to: The recipient address. - /// - amount: The amount to transfer in wei (as a string to handle large numbers). - /// - chainId: Optional chain ID (auto-detected if not provided). - /// - rpcUrl: Optional RPC URL (defaults to Ethereum mainnet if not provided). - /// - Returns: Transaction result containing hash and details. func transfer( walletId: String, to: String, @@ -298,14 +203,7 @@ public extension ParaManager { let rpcUrl: String? } - let params = TransferParams( - walletId: walletId, - toAddress: to, - amount: amount, - chainId: chainId, - rpcUrl: rpcUrl - ) - + let params = TransferParams(walletId: walletId, toAddress: to, amount: amount, chainId: chainId, rpcUrl: rpcUrl) let result = try await postMessage(method: "transfer", payload: params) let dict = try decodeResult(result, expectedType: [String: Any].self, method: "transfer") @@ -317,4 +215,15 @@ public extension ParaManager { chainId: dict["chainId"] as? String ?? "" ) } + + // MARK: - Private + + /// Checks a bridge response for pendingTransactionId (permissions denial). + /// Calls the transaction review handler if set, then throws. + private func checkForTransactionDenial(_ dict: [String: Any]) throws { + guard let pendingTransactionId = dict["pendingTransactionId"] as? String else { return } + let reviewUrl = dict["transactionReviewUrl"] as? String + if let reviewUrl { transactionReviewHandler?(reviewUrl) } + throw ParaError.transactionDenied(pendingTransactionId: pendingTransactionId, transactionReviewUrl: reviewUrl) + } } diff --git a/Sources/ParaSwift/Core/ParaTypes.swift b/Sources/ParaSwift/Core/ParaTypes.swift index 459531a..2770e29 100644 --- a/Sources/ParaSwift/Core/ParaTypes.swift +++ b/Sources/ParaSwift/Core/ParaTypes.swift @@ -22,6 +22,8 @@ public enum ParaError: Error, CustomStringConvertible, LocalizedError { case error(String) /// Feature not implemented yet. case notImplemented(String) + /// A signing operation was denied by a permissions policy and requires user approval. + case transactionDenied(pendingTransactionId: String, transactionReviewUrl: String?) public var description: String { switch self { @@ -33,6 +35,8 @@ public enum ParaError: Error, CustomStringConvertible, LocalizedError { "An error occurred: \(info)" case let .notImplemented(feature): "Feature not implemented: \(feature)" + case let .transactionDenied(id, _): + "Transaction requires approval (pending: \(id))" } } @@ -47,6 +51,8 @@ public enum ParaError: Error, CustomStringConvertible, LocalizedError { return info case let .notImplemented(feature): return "Feature not implemented: \(feature)" + case let .transactionDenied(id, _): + return "Transaction requires approval (pending: \(id))" } } } diff --git a/Sources/ParaSwift/ParaManager+Solana.swift b/Sources/ParaSwift/ParaManager+Solana.swift index 070ebdc..002132e 100644 --- a/Sources/ParaSwift/ParaManager+Solana.swift +++ b/Sources/ParaSwift/ParaManager+Solana.swift @@ -23,14 +23,14 @@ public extension ParaManager { /// - Parameters: /// - walletId: The ID of the Solana wallet to use for signing /// - base64Tx: The base64-encoded serialized transaction - /// - Returns: A SigningResult — either `.success` with the signature or `.denied` with the review URL. + /// - Returns: A SignatureResult containing the signature /// - Throws: ParaWebViewError if signing fails func signSolanaSerializedTransaction( walletId: String, base64Tx: String - ) async throws -> SigningResult { + ) async throws -> SignatureResult { let transaction = PreSerializedTransaction(data: base64Tx) - + return try await signTransaction( walletId: walletId, transaction: transaction