diff --git a/.github/actions/run_tests_without_building/action.yml b/.github/actions/run_tests_without_building/action.yml index 9893288d6..b8156f175 100644 --- a/.github/actions/run_tests_without_building/action.yml +++ b/.github/actions/run_tests_without_building/action.yml @@ -35,6 +35,9 @@ inputs: js-client-api-host: description: 'JS Client Api Host' required: false + artifact-download-token: + description: 'Token with access to download DerivedData artifacts' + required: false runs: @@ -47,6 +50,7 @@ runs: name: main-derivedData workflow: build_artifacts.yml repo: 'reown-com/reown-swift' + github_token: ${{ inputs.artifact-download-token != '' && inputs.artifact-download-token || github.token }} if_no_artifact_found: warn - name: Untar DerivedDataCache diff --git a/.gitignore b/.gitignore index 1b04b4db5..fded313ad 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ DerivedDataCache test_results/ Sources/WalletConnectModal/Secrets/secrets.json + +# Provisioning Profiles +*.mobileprovision \ No newline at end of file diff --git a/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift b/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift index 1c8fd2cef..a0da4fbf9 100644 --- a/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift +++ b/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift @@ -165,8 +165,8 @@ private enum Stub { } static let tx = [Transaction(from: "0x9b2055d370f73ec7d8a03e965129118dc8f5bf83", - to: "0x9b2055d370f73ec7d8a03e965129118dc8f5bf83", - data: "0x", + to: "0x521B4C065Bbdbe3E20B3727340730936912DfA46", + data: "0x7c616fe60000000000000000000000000000000000000000000000000000000067741500", gasLimit: "0x5208", gasPrice: "0x013e3d2ed4", value: "0x00", @@ -174,8 +174,8 @@ private enum Stub { static func tx(from: String) -> [Transaction] { return [Transaction(from: from, - to: "0x9b2055d370f73ec7d8a03e965129118dc8f5bf83", - data: "0x", + to: "0x521B4C065Bbdbe3E20B3727340730936912DfA46", + data: "0x7c616fe60000000000000000000000000000000000000000000000000000000067741500", gasLimit: "0x5208", gasPrice: "0x013e3d2ed4", value: "0x186A0", diff --git a/Example/DApp/Modules/Sign/SignInteractor.swift b/Example/DApp/Modules/Sign/SignInteractor.swift index 793292c44..9c522fcda 100644 --- a/Example/DApp/Modules/Sign/SignInteractor.swift +++ b/Example/DApp/Modules/Sign/SignInteractor.swift @@ -16,7 +16,7 @@ enum Proposal { "eip155": ProposalNamespace( chains: [ Blockchain("eip155:137")!, - Blockchain("eip155:1")! + Blockchain("eip155:10")! ], methods: [ "eth_sendTransaction", diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index ccc3f9d32..f1e30ae9f 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -2518,7 +2518,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.dapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.dapp"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "com.walletconnect.dapp AppStore"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2531,9 +2531,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = PNDecryptionService/PNDecryptionService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 184; - DEVELOPMENT_TEAM = W5R8AG9K22; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PNDecryptionService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = PNDecryptionService; @@ -2548,6 +2550,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp.PNDecryptionService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "com.walletconnect.walletapp.PNDecryptionService AppStore"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -2580,7 +2583,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp.PNDecryptionService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp.PNDecryptionService"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "com.walletconnect.walletapp.PNDecryptionService AppStore"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -2734,7 +2737,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "com.walletconnect.walletapp AppStore"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 45f90fee7..f74963062 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -270,15 +270,6 @@ "revision": "4232d34efa49f633ba61afde365d3896fc7f8740", "version": "2.15.0" } - }, - { - "package": "Yttrium", - "repositoryURL": "https://github.com/reown-com/yttrium", - "state": { - "branch": null, - "revision": "ed8e8f5af2029406263be5993e484c3a69c1db7a", - "version": "0.9.68" - } } ] }, diff --git a/Example/Shared/Signer/EOASigner.swift b/Example/Shared/Signer/EOASigner.swift index 286197aac..1b84a6184 100644 --- a/Example/Shared/Signer/EOASigner.swift +++ b/Example/Shared/Signer/EOASigner.swift @@ -16,7 +16,7 @@ final class EOASigner { return signer.signTypedData(request.params) case "eth_sendTransaction": - return try signer.sendTransaction(request.params) + return try await signer.sendTransaction(request: request) case "solana_signTransaction": return SOLSigner.signTransaction(request.params) diff --git a/Example/Shared/Signer/ETHSigner.swift b/Example/Shared/Signer/ETHSigner.swift index 7fe1b5af9..f7f5f51c1 100644 --- a/Example/Shared/Signer/ETHSigner.swift +++ b/Example/Shared/Signer/ETHSigner.swift @@ -1,8 +1,44 @@ import Foundation import Commons import Web3 +import WalletConnectSign +import YttriumUtilsWrapper struct ETHSigner { + enum Errors: LocalizedError { + case invalidTransactionParams + + var errorDescription: String? { + switch self { + case .invalidTransactionParams: + return "Invalid parameters for eth_sendTransaction" + } + } + } + + private struct EthSendTransactionParams: Codable { + let from: String? + let to: String? + let value: String? + let data: String? + let gas: String? + let gasLimit: String? + let gasPrice: String? + let maxFeePerGas: String? + let maxPriorityFeePerGas: String? + let nonce: String? + } + + private static let evmSigningClient: EvmSigningClient = { + let metadata = YttriumUtils.PulseMetadata( + url: nil, + bundleId: Bundle.main.bundleIdentifier ?? "", + sdkVersion: "reown-swift-\(EnvironmentInfo.sdkName)", + sdkPlatform: "mobile" + ) + return EvmSigningClient(projectId: InputConfig.projectId, pulseMetadata: metadata) + }() + private let importAccount: ImportAccount init(importAccount: ImportAccount) { @@ -22,7 +58,7 @@ struct ETHSigner { let messageToSign = params[0] // Determine if the message is hex-encoded or plain text - let dataToSign: Bytes + let dataToSign: [UInt8] if messageToSign.hasPrefix("0x") { // Hex-encoded message, remove "0x" and convert let messageData = Data(hex: String(messageToSign.dropFirst(2))) @@ -41,15 +77,15 @@ struct ETHSigner { func signHash(_ hashToSign: String) throws -> String { - let dataToSign: Bytes + let dataToSign: [UInt8] if hashToSign.hasPrefix("0x") { // Hex-encoded message, remove "0x" and convert let messageData = Data(hex: String(hashToSign.dropFirst(2))) - dataToSign = messageData.bytes + dataToSign = Array(messageData) } else { // Plain text message, convert directly to data let messageData = Data(hashToSign.utf8) - dataToSign = messageData.bytes + dataToSign = Array(messageData) } let (v, r, s) = try! privateKey.sign(hash: dataToSign) @@ -57,28 +93,75 @@ struct ETHSigner { return result } - func signTypedData(_ params: AnyCodable) -> AnyCodable { + func signTypedData(_ params: AnyCodable) -> AnyCodable { // TODO: implement typed data signing let result = "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" return AnyCodable(result) } - func sendTransaction(_ params: AnyCodable) throws -> AnyCodable { -// let params = try params.get([Tx].self) -// var transaction = params[0] -// transaction.gas = EthereumQuantity(quantity: BigUInt("1234")) -// transaction.nonce = EthereumQuantity(quantity: BigUInt("0")) -// transaction.gasPrice = EthereumQuantity(quantity: BigUInt(0)) -// print(transaction.description) -// let signedTx = try transaction.sign(with: self.privateKey, chainId: 4) -// let (r, s, v) = (signedTx.r, signedTx.s, signedTx.v) -// let result = r.hex() + s.hex().dropFirst(2) + String(v.quantity, radix: 16) - return AnyCodable("0xabcd12340000000000000000000000111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000f0") + func sendTransaction(request: Request) async throws -> AnyCodable { + guard let tx = try request.params.get([EthSendTransactionParams].self).first else { + throw Errors.invalidTransactionParams + } + + let fromAddress = tx.from?.isEmpty == false ? tx.from! : address + let transactionParams = SignAndSendParams( + chainId: request.chainId.absoluteString, + from: normalizeAddress(fromAddress) ?? fromAddress, + to: normalizeAddress(tx.to), + value: ensureHexPrefix(tx.value), + data: normalizedDataHex(tx.data), + gasLimit: ensureHexPrefix(coalesce(tx.gasLimit, tx.gas)), + maxFeePerGas: ensureHexPrefix(coalesce(tx.maxFeePerGas, tx.gasPrice)), + maxPriorityFeePerGas: ensureHexPrefix(tx.maxPriorityFeePerGas), + nonce: ensureHexPrefix(tx.nonce) + ) + + let signer = ensureHexPrefix(importAccount.privateKey) + let result = try await Self.evmSigningClient.signAndSend( + params: transactionParams, + signer: signer + ) + + return AnyCodable(result.transactionHash) } - private func dataToHash(_ data: Data) -> Bytes { + private func dataToHash(_ data: Data) -> [UInt8] { let prefix = "\u{19}Ethereum Signed Message:\n" let prefixData = (prefix + String(data.count)).data(using: .utf8)! let prefixedMessageData = prefixData + data - return .init(hex: prefixedMessageData.toHexString()) + return Array(prefixedMessageData) + } + + private func ensureHexPrefix(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + if value.hasPrefix("0x") || value.hasPrefix("0X") { + return value.lowercased().hasPrefix("0x") ? value : "0x" + value.dropFirst(2) + } + return "0x" + value + } + + private func ensureHexPrefix(_ value: String) -> String { + return ensureHexPrefix(Optional(value)) ?? value + } + + private func normalizedDataHex(_ value: String?) -> String? { + guard let value = ensureHexPrefix(value) else { + return "0x" + } + return value + } + + private func normalizeAddress(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return ensureHexPrefix(value) + } + + private func coalesce(_ values: String?...) -> String? { + for value in values { + if let value, !value.isEmpty { + return value + } + } + return nil } } diff --git a/Example/Shared/Signer/StacksSigner.swift b/Example/Shared/Signer/StacksSigner.swift index 18ffb5a41..a02c1191d 100644 --- a/Example/Shared/Signer/StacksSigner.swift +++ b/Example/Shared/Signer/StacksSigner.swift @@ -56,9 +56,9 @@ final class StacksSigner { private let stacksClient: StacksClient init() { - let pulseMetadata = PulseMetadata( + let pulseMetadata = YttriumUtils.PulseMetadata( url: nil, - bundleId: Bundle.main.bundleIdentifier ?? "", + bundleId: Bundle.main.bundleIdentifier, sdkVersion: "reown-swift-\(EnvironmentInfo.sdkName)", sdkPlatform: "mobile" ) diff --git a/Example/Shared/Signer/SuiSigner.swift b/Example/Shared/Signer/SuiSigner.swift index 9fcb4acca..045503940 100644 --- a/Example/Shared/Signer/SuiSigner.swift +++ b/Example/Shared/Signer/SuiSigner.swift @@ -46,9 +46,9 @@ final class SuiSigner { private static var suiClient: SuiClient? static func initialize(projectId: String) { - let pulseMetadata = PulseMetadata( + let pulseMetadata = YttriumUtils.PulseMetadata( url: nil, - bundleId: Bundle.main.bundleIdentifier ?? "", + bundleId: Bundle.main.bundleIdentifier, sdkVersion: "reown-swift-\(EnvironmentInfo.sdkName)", sdkPlatform: "mobile" ) diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 950e842ee..26749247f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -3,27 +3,49 @@ import Combine import Web3 import ReownWalletKit +import YttriumUtilsWrapper final class SessionRequestPresenter: ObservableObject { private let interactor: SessionRequestInteractor private let router: SessionRequestRouter private let importAccount: ImportAccount + private let typedDataMethods: Set = [ + "eth_signTypedData", + "eth_signTypedData_v3", + "eth_signTypedData_v4" + ] + private let transactionMethods: Set = [ + "eth_sendTransaction", + "eth_signTransaction" + ] let sessionRequest: Request let session: Session? let validationStatus: VerifyContext.ValidationStatus? + // Clear signing (EIP-7730) display model + @Published var clearSigningIntent: String? + @Published var clearSigningItems: [(label: String, value: String)] = [] + @Published var clearSigningInterpolatedIntent: String? + @Published var clearSigningWarnings: [String] = [] + @Published var clearSigningRawSelector: String? + @Published var clearSigningRawArgs: [String] = [] + var message: String { - guard let messages = try? sessionRequest.params.get([String].self), - let firstMessage = messages.first else { - return String(describing: sessionRequest.params.value) + if let typedPayload = resolveTypedDataPayloadForDisplay() { + return prettyPrintedJSON(from: typedPayload) ?? typedPayload } - // Attempt to decode the message if it's hex-encoded - let decodedMessage = String(data: Data(hex: firstMessage), encoding: .utf8) + if let messages = try? sessionRequest.params.get([String].self), + let firstMessage = messages.first { + if let decodedMessage = String(data: Data(hex: firstMessage), encoding: .utf8), + !decodedMessage.isEmpty { + return decodedMessage + } + return firstMessage + } - // Return the decoded message if available, else return the original message - return decodedMessage?.isEmpty == false ? decodedMessage! : firstMessage + return String(describing: sessionRequest.params.value) } @@ -89,6 +111,7 @@ final class SessionRequestPresenter: ObservableObject { // MARK: - Private functions private extension SessionRequestPresenter { func setupInitialState() { + computeClearSigningPreview() WalletKit.instance.requestExpirationPublisher .receive(on: DispatchQueue.main) .sink { [weak self] requestId in @@ -98,6 +121,223 @@ private extension SessionRequestPresenter { } }.store(in: &disposeBag) } + + struct TxLike: Codable { + let to: String? + let data: String? + } + + enum ClearSigningContext { + case typedData(json: String) + case transaction(chainId: UInt64, tx: TxLike) + } + + struct SessionRequestEnvelope: Codable { + struct InnerRequest: Codable { + let method: String + let params: [AnyCodable] + } + + let chainId: String? + let request: InnerRequest + } + + func computeClearSigningPreview() { + clearSigningInterpolatedIntent = nil + guard let context = resolveClearSigningContext() else { return } + + switch context { + case .typedData(let json): + renderTypedDataPreview(typedDataJson: json) + case .transaction(let chainId, let tx): + renderTransactionPreview(chainId: chainId, transaction: tx) + } + } + + func resolveClearSigningContext() -> ClearSigningContext? { + if typedDataMethods.contains(sessionRequest.method), + let payload = extractTypedDataPayload(from: sessionRequest.params) { + return .typedData(json: payload) + } + + if transactionMethods.contains(sessionRequest.method), + let chainIdNumber = UInt64(sessionRequest.chainId.reference), + let tx = extractTransaction(from: sessionRequest.params) { + return .transaction(chainId: chainIdNumber, tx: tx) + } + + if sessionRequest.method == "wc_sessionRequest", + let envelope = decodeSessionRequestEnvelope(from: sessionRequest.params) { + let innerMethod = envelope.request.method + let chainReference = envelope.chainId ?? sessionRequest.chainId.reference + + if typedDataMethods.contains(innerMethod), + let payload = extractTypedDataPayload(from: envelope.request.params) { + return .typedData(json: payload) + } + + if transactionMethods.contains(innerMethod), + let chainIdNumber = parseChainId(from: chainReference), + let tx = extractTransaction(from: envelope.request.params) { + return .transaction(chainId: chainIdNumber, tx: tx) + } + } + + return nil + } + + func renderTransactionPreview(chainId: UInt64, transaction: TxLike) { + guard let to = transaction.to, let calldataHex = transaction.data else { return } + + do { + let displayModel = try clearSigningFormat( + chainId: chainId, + to: to, + calldataHex: calldataHex + ) + + clearSigningIntent = displayModel.intent + clearSigningInterpolatedIntent = displayModel.interpolatedIntent + clearSigningItems = displayModel.items.map { ($0.label, $0.value) } + clearSigningWarnings = displayModel.warnings + if let raw = displayModel.raw { + clearSigningRawSelector = raw.selector + clearSigningRawArgs = raw.args + } else { + clearSigningRawSelector = nil + clearSigningRawArgs = [] + } + } catch { + errorMessage = error.localizedDescription + showError = true + } + } + + func renderTypedDataPreview(typedDataJson: String) { + do { + let displayModel = try clearSigningFormatTyped(typedDataJson: typedDataJson) + + clearSigningIntent = displayModel.intent + clearSigningInterpolatedIntent = displayModel.interpolatedIntent + clearSigningItems = displayModel.items.map { ($0.label, $0.value) } + clearSigningWarnings = displayModel.warnings + clearSigningRawSelector = nil + clearSigningRawArgs = [] + } catch { + errorMessage = error.localizedDescription + showError = true + } + } + + func decodeSessionRequestEnvelope(from params: AnyCodable) -> SessionRequestEnvelope? { + if let rawString = params.value as? String, + let data = rawString.data(using: .utf8) { + return try? JSONDecoder().decode(SessionRequestEnvelope.self, from: data) + } + + if let dictionary = params.value as? [String: Any], + JSONSerialization.isValidJSONObject(dictionary), + let data = try? JSONSerialization.data(withJSONObject: dictionary, options: [.sortedKeys]) { + return try? JSONDecoder().decode(SessionRequestEnvelope.self, from: data) + } + + return try? params.get(SessionRequestEnvelope.self) + } + + func parseChainId(from caip2: String?) -> UInt64? { + guard let caip2 = caip2 else { return nil } + let components = caip2.split(separator: ":") + guard let reference = components.last else { return nil } + return UInt64(reference) + } + + func extractTransaction(from params: AnyCodable) -> TxLike? { + guard let txs = try? params.get([TxLike].self) else { return nil } + return txs.first + } + + func extractTransaction(from params: [AnyCodable]) -> TxLike? { + let rawValues = params.map { $0.value } + guard JSONSerialization.isValidJSONObject(rawValues), + let data = try? JSONSerialization.data(withJSONObject: rawValues, options: []) else { return nil } + let txs = try? JSONDecoder().decode([TxLike].self, from: data) + return txs?.first + } + + func extractTypedDataPayload(from params: AnyCodable) -> String? { + if let stringArray = try? params.get([String].self), stringArray.count >= 2 { + return sanitizeTypedDataPayload(stringArray[1]) + } + + if let anyArray = try? params.get([AnyCodable].self) { + return extractTypedDataPayload(from: anyArray) + } + + return nil + } + + func extractTypedDataPayload(from params: [AnyCodable]) -> String? { + guard params.count >= 2 else { return nil } + return coerceJSON(from: params[1].value) + } + + func sanitizeTypedDataPayload(_ payload: String) -> String? { + let trimmed = payload.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + + func coerceJSON(from value: Any) -> String? { + if let stringValue = value as? String { + return sanitizeTypedDataPayload(stringValue) + } + + if let dictionary = value as? [String: Any], + JSONSerialization.isValidJSONObject(dictionary), + let data = try? JSONSerialization.data(withJSONObject: dictionary, options: [.sortedKeys]), + let jsonString = String(data: data, encoding: .utf8) { + return jsonString + } + + if let array = value as? [Any], + JSONSerialization.isValidJSONObject(array), + let data = try? JSONSerialization.data(withJSONObject: array, options: [.sortedKeys]), + let jsonString = String(data: data, encoding: .utf8) { + return jsonString + } + + if let anyCodable = value as? AnyCodable { + return sanitizeTypedDataPayload(anyCodable.stringRepresentation) + } + + return nil + } + + func resolveTypedDataPayloadForDisplay() -> String? { + if typedDataMethods.contains(sessionRequest.method), + let payload = extractTypedDataPayload(from: sessionRequest.params) { + return payload + } + + if sessionRequest.method == "wc_sessionRequest", + let envelope = decodeSessionRequestEnvelope(from: sessionRequest.params), + typedDataMethods.contains(envelope.request.method), + let payload = extractTypedDataPayload(from: envelope.request.params) { + return payload + } + + return nil + } + + func prettyPrintedJSON(from text: String) -> String? { + guard let data = text.data(using: .utf8), + let object = try? JSONSerialization.jsonObject(with: data) else { + return nil + } + if let pretty = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]) { + return String(data: pretty, encoding: .utf8) + } + return nil + } } // MARK: - SceneViewModel diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift index 1f0ba6ae0..4f8b6dc8b 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift @@ -29,88 +29,105 @@ struct SessionRequestView: View { Image("header") .resizable() .scaledToFit() - - if LinkModeTopicsStorage.shared.containsTopic(presenter.sessionRequest.topic) { - Text("LINK MODE") - .font(.system(size: 16, weight: .bold)) - .foregroundColor(.white) - .padding(.horizontal, 20) - .padding(.vertical, 8) - .background(Color.blue) - .cornerRadius(20) - .padding(.vertical, 5) - } - - Text(presenter.sessionRequest.method) - .foregroundColor(.grey8) - .font(.system(size: 22, weight: .bold, design: .rounded)) - .padding(.top, 10) - - if case .valid = presenter.validationStatus { - HStack { - Image(systemName: "checkmark.seal.fill") - .font(.system(size: 14, weight: .semibold, design: .rounded)) - .foregroundColor(.blue) - - Text(presenter.session?.peer.url ?? "") + ScrollView { + VStack(spacing: 0) { + if LinkModeTopicsStorage.shared.containsTopic(presenter.sessionRequest.topic) { + Text("LINK MODE") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 8) + .background(Color.blue) + .cornerRadius(20) + .padding(.vertical, 5) + } + + Text(presenter.sessionRequest.method) .foregroundColor(.grey8) - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .lineSpacing(4) - } - .padding(.top, 8) - } else { - Text(presenter.session?.peer.url ?? "") - .foregroundColor(.grey8) - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .multilineTextAlignment(.center) - .lineSpacing(4) - .padding(.top, 8) - } - - switch presenter.validationStatus { - case .unknown: - verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) - - case .invalid: - verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) - - case .scam: - verifyBadgeView(imageName: "exclamationmark.shield.fill", title: "Security risk", color: .red) - - default: - EmptyView() - } - - if presenter.message != "[:]" { - authRequestView() - } - - switch presenter.validationStatus { - case .invalid: - verifyDescriptionView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .red) - - case .unknown: - verifyDescriptionView(imageName: "exclamationmark.circle.fill", title: "Unknown domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .orange) - - case .scam: - verifyDescriptionView(imageName: "exclamationmark.shield.fill", title: "Security risk", description: "This website is flagged as unsafe by multiple security providers. Leave immediately to protect your assets.", color: .red) - - default: - EmptyView() - } - - if case .scam = presenter.validationStatus { - VStack(spacing: 20) { - declineButton() - allowButton() - } - .padding(.top, 25) - } else { - HStack { - declineButton() - allowButton() + .font(.system(size: 22, weight: .bold, design: .rounded)) + .padding(.top, 10) + + if case .valid = presenter.validationStatus { + HStack { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.blue) + + Text(presenter.session?.peer.url ?? "") + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineSpacing(4) + } + .padding(.top, 8) + } else { + Text(presenter.session?.peer.url ?? "") + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(.top, 8) + } + + switch presenter.validationStatus { + case .unknown: + verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) + + case .invalid: + verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) + + case .scam: + verifyBadgeView(imageName: "exclamationmark.shield.fill", title: "Security risk", color: .red) + + default: + EmptyView() + } + + if presenter.clearSigningIntent != nil || presenter.clearSigningInterpolatedIntent != nil || !presenter.clearSigningItems.isEmpty || presenter.clearSigningRawSelector != nil { + intentSectionView() + } + + if presenter.message != "[:]" { + authRequestView() + } + + // Surface warnings from clear signing as secondary notices + if !presenter.clearSigningWarnings.isEmpty { + VStack(spacing: 10) { + ForEach(0.. some View { + VStack { + VStack(alignment: .leading) { + Text("Intent") + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(.whiteBackground) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color.grey70) + .cornerRadius(28, corners: .allCorners) + .padding(.leading, 15) + .padding(.top, 9) + + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 10) { + if let intent = presenter.clearSigningIntent { + VStack(alignment: .leading, spacing: 4) { + Text("Intent") + .foregroundColor(.grey50) + .font(.system(size: 12, weight: .semibold, design: .rounded)) + Text(intent) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + if let summary = presenter.clearSigningInterpolatedIntent { + VStack(alignment: .leading, spacing: 4) { + Text(summary) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + } else if !presenter.clearSigningItems.isEmpty { + ForEach(0.. some View { VStack { VStack(alignment: .leading) { diff --git a/Package.swift b/Package.swift index 9fb257872..17fda8894 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = false +let yttriumDebug = true // Define dependencies array