Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ABW-2128 Verify Dapps #711

Merged
merged 10 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics",
"location" : "https://github.com/apple/swift-numerics.git",
"state" : {
"revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
"version" : "1.0.2"
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ package.addModules([
"GatewaysClient",
"AppPreferencesClient",
"DappInteractionClient",
"ROLAClient",
],
tests: .no
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ extension AppPreferencesClient {
try mutateDisplay(&preferences.display)
}
}

public func isDeveloperModeEnabled() async -> Bool {
await extractProfileSnapshot().appPreferences.security.isDeveloperModeEnabled
}
}

// MARK: AppPreferencesClient.Error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ extension DappInteractionClient {
case wrongNetworkID(connectorExtensionSent: NetworkID, walletUses: NetworkID)
case invalidDappDefinitionAddress(gotStringWhichIsAnInvalidAccountAddress: String)
case invalidOrigin(invalidURLString: String)
case dAppValidationError
case badContent(BadContent)
case p2pError(String)
public enum BadContent: Sendable, Hashable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ComposableArchitecture // actually CasePaths... but CI fails if we do `im
import DappInteractionClient
import GatewaysClient
import RadixConnectClient
import ROLAClient
import SharedModels

// MARK: - DappInteractionClient + DependencyKey
Expand Down Expand Up @@ -62,72 +63,82 @@ extension DappInteractionClient {
) async -> ValidatedDappRequest {
@Dependency(\.appPreferencesClient) var appPreferencesClient
@Dependency(\.gatewaysClient) var gatewaysClient
@Dependency(\.rolaClient) var rolaClient

return await {
let nonValidated: P2P.Dapp.RequestUnvalidated
do {
nonValidated = try message.result.get()
} catch {
return .invalid(.p2pError(error.legibleLocalizedDescription))
}
let nonValidated: P2P.Dapp.RequestUnvalidated
do {
nonValidated = try message.result.get()
} catch {
return .invalid(.p2pError(error.legibleLocalizedDescription))
}

let nonvalidatedMeta = nonValidated.metadata
guard P2P.Dapp.currentVersion == nonvalidatedMeta.version else {
return .invalid(.incompatibleVersion(connectorExtensionSent: nonvalidatedMeta.version, walletUses: P2P.Dapp.currentVersion))
}
let currentNetworkID = await gatewaysClient.getCurrentNetworkID()
guard currentNetworkID == nonValidated.metadata.networkId else {
return .invalid(.wrongNetworkID(connectorExtensionSent: nonvalidatedMeta.networkId, walletUses: currentNetworkID))
}
let nonvalidatedMeta = nonValidated.metadata
guard P2P.Dapp.currentVersion == nonvalidatedMeta.version else {
return .invalid(.incompatibleVersion(connectorExtensionSent: nonvalidatedMeta.version, walletUses: P2P.Dapp.currentVersion))
}
let currentNetworkID = await gatewaysClient.getCurrentNetworkID()
guard currentNetworkID == nonValidated.metadata.networkId else {
return .invalid(.wrongNetworkID(connectorExtensionSent: nonvalidatedMeta.networkId, walletUses: currentNetworkID))
}

let dappDefinitionAddress: DappDefinitionAddress
do {
dappDefinitionAddress = try DappDefinitionAddress(
validatingAddress: nonValidated.metadata.dAppDefinitionAddress
)
} catch {
return .invalid(.invalidDappDefinitionAddress(gotStringWhichIsAnInvalidAccountAddress: nonvalidatedMeta.dAppDefinitionAddress))
}
let dappDefinitionAddress: DappDefinitionAddress
do {
dappDefinitionAddress = try DappDefinitionAddress(
validatingAddress: nonValidated.metadata.dAppDefinitionAddress
)
} catch {
return .invalid(.invalidDappDefinitionAddress(gotStringWhichIsAnInvalidAccountAddress: nonvalidatedMeta.dAppDefinitionAddress))
}

if case let .request(readRequest) = nonValidated.items {
switch readRequest {
case let .authorized(authorized):
if authorized.oneTimeAccounts?.numberOfAccounts.isValid == false {
return .invalid(.badContent(.numberOfAccountsInvalid))
}
if authorized.ongoingAccounts?.numberOfAccounts.isValid == false {
return .invalid(.badContent(.numberOfAccountsInvalid))
}
case let .unauthorized(unauthorized):
if unauthorized.oneTimeAccounts?.numberOfAccounts.isValid == false {
return .invalid(.badContent(.numberOfAccountsInvalid))
}
if case let .request(readRequest) = nonValidated.items {
switch readRequest {
case let .authorized(authorized):
if authorized.oneTimeAccounts?.numberOfAccounts.isValid == false {
return .invalid(.badContent(.numberOfAccountsInvalid))
}
if authorized.ongoingAccounts?.numberOfAccounts.isValid == false {
return .invalid(.badContent(.numberOfAccountsInvalid))
}
case let .unauthorized(unauthorized):
if unauthorized.oneTimeAccounts?.numberOfAccounts.isValid == false {
return .invalid(.badContent(.numberOfAccountsInvalid))
}
}
}

guard let originURL = URL(string: nonvalidatedMeta.origin),
let nonEmptyOriginURLString = NonEmptyString(rawValue: nonvalidatedMeta.origin)
else {
return .invalid(.invalidOrigin(invalidURLString: nonvalidatedMeta.origin))
}

let origin = DappOrigin(urlString: nonEmptyOriginURLString, url: originURL)

guard
let originURL = URL(string: nonvalidatedMeta.origin),
let nonEmptyOriginURLString = NonEmptyString(rawValue: nonvalidatedMeta.origin)
else {
return .invalid(.invalidOrigin(invalidURLString: nonvalidatedMeta.origin))
let metadataValidDappDefAddress = P2P.Dapp.Request.Metadata(
version: nonvalidatedMeta.version,
networkId: nonvalidatedMeta.networkId,
origin: origin,
dAppDefinitionAddress: dappDefinitionAddress
)

let isDeveloperModeEnabled = await appPreferencesClient.isDeveloperModeEnabled()
if !isDeveloperModeEnabled {
do {
try await rolaClient.performDappDefinitionVerification(metadataValidDappDefAddress)
try await rolaClient.performWellKnownFileCheck(metadataValidDappDefAddress)
} catch {
loggerGlobal.warning("\(error)")
return .invalid(.dAppValidationError)
}
let origin = DappOrigin(urlString: nonEmptyOriginURLString, url: originURL)
}

let metadataValidDappDefAddres = P2P.Dapp.Request.Metadata(
version: nonvalidatedMeta.version,
networkId: nonvalidatedMeta.networkId,
origin: origin,
dAppDefinitionAddress: dappDefinitionAddress
return .valid(.init(
route: message.route,
request: .init(
id: nonValidated.id,
items: nonValidated.items,
metadata: metadataValidDappDefAddress
)

return .valid(.init(
route: message.route,
request: .init(
id: nonValidated.id,
items: nonValidated.items,
metadata: metadataValidDappDefAddres
)
))
}()
))
}
}
6 changes: 6 additions & 0 deletions Sources/Clients/GatewayAPI/GatewayAPI+Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ extension GatewayAPI.EntityMetadataCollection {
case accountTypeNotDappDefinition
case missingClaimedEntities
case entityNotClaimed
case missingClaimedWebsites
case websiteNotClaimed
case dAppDefinitionNotReciprocating

public var description: String {
Expand All @@ -173,6 +175,10 @@ extension GatewayAPI.EntityMetadataCollection {
return "The dapp definition has no claimed_entities key"
case .entityNotClaimed:
return "The entity is not claimed by the dApp definition"
case .missingClaimedWebsites:
return "The dapp definition has no claimed_websites key"
case .websiteNotClaimed:
return "The website is not claimed by the dApp definition"
case .dAppDefinitionNotReciprocating:
return "This dApp definition does not point back to the dApp definition that claims to be associated with it"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,6 @@ extension GatewayAPIClient {
return item
}

/// Extracts the dApp definition from a component, if it has one
public func getDappDefinition(_ component: ComponentAddress) async throws -> GatewayAPI.EntityMetadataCollection {
let dappDefinitionAddress = try await getDappDefinitionAddress(component)
return try await getDappMetadata(dappDefinitionAddress)
.validating(dAppComponent: component)
}

/// Extracts the dApp definition address from a component, if one is present
public func getDappDefinitionAddress(_ component: ComponentAddress) async throws -> DappDefinitionAddress {
let entityMetadata = try await getEntityMetadata(component.address, [.dappDefinition])
Expand All @@ -96,14 +89,27 @@ extension GatewayAPIClient {
}

/// Fetches the metadata for a dApp. If the component address is supplied, it validates that it is contained in `claimed_entities`
public func getDappMetadata(_ dappDefinition: DappDefinitionAddress) async throws -> GatewayAPI.EntityMetadataCollection {
let dappDefinition = try await getEntityMetadata(dappDefinition.address, [.accountType])

guard dappDefinition.accountType == .dappDefinition else {
throw GatewayAPI.EntityMetadataCollection.MetadataError.accountTypeNotDappDefinition
public func getDappMetadata(
_ dappDefinition: DappDefinitionAddress,
validatingDappComponent component: ComponentAddress? = nil,
validatingDappDefinitionAddress dappDefinitionAddress: DappDefinitionAddress? = nil,
validatingWebsite website: URL? = nil
) async throws -> GatewayAPI.EntityMetadataCollection {
let dappMetadata = try await getEntityMetadata(dappDefinition.address, [.accountType, .name, .description, .iconURL, .claimedEntities, .claimedWebsites, .dappDefinitions, .symbol])

try dappMetadata.validateAccountType()

if let component {
try dappMetadata.validate(dAppComponent: component)
}
if let dappDefinitionAddress {
try dappMetadata.validate(dAppDefinitionAddress: dappDefinitionAddress)
}
if let website {
try dappMetadata.validate(website: website)
}

return dappDefinition
return dappMetadata
}

// The maximum number of addresses the `getEntityDetails` can accept
Expand Down Expand Up @@ -135,25 +141,40 @@ extension GatewayAPIClient {
}

extension GatewayAPI.EntityMetadataCollection {
public func validating(dAppComponent component: ComponentAddress) throws -> GatewayAPI.EntityMetadataCollection {
/// Check that `account_type` is present and equal to `dapp_definition`
public func validateAccountType() throws {
guard accountType == .dappDefinition else {
throw GatewayAPI.EntityMetadataCollection.MetadataError.accountTypeNotDappDefinition
}
}

/// Check that `claimed_entities` is present and contains the provided `ComponentAddress`
public func validate(dAppComponent component: ComponentAddress) throws {
guard let claimedEntities else {
throw GatewayAPI.EntityMetadataCollection.MetadataError.missingClaimedEntities
}

guard claimedEntities.contains(component.address) else {
throw GatewayAPI.EntityMetadataCollection.MetadataError.entityNotClaimed
}
}

return self
/// Check that `claimed_websites`is present and contains the provided website `URL`
public func validate(website: URL) throws {
guard let claimedWebsites else {
throw GatewayAPI.EntityMetadataCollection.MetadataError.missingClaimedWebsites
}

guard claimedWebsites.contains(website) else {
throw GatewayAPI.EntityMetadataCollection.MetadataError.websiteNotClaimed
}
}

/// Validate that the entity that owns this metadata gives the provided dAppDefinitionAddress
public func validating(dAppDefinitionAddress: DappDefinitionAddress) throws -> GatewayAPI.EntityMetadataCollection {
guard dappDefinition == dAppDefinitionAddress.address else {
/// Validate that `dapp_definitions` is present and contains the provided `dAppDefinitionAddress`
public func validate(dAppDefinitionAddress: DappDefinitionAddress) throws {
guard let dappDefinitions, dappDefinitions.contains(dAppDefinitionAddress.address) else {
throw GatewayAPI.EntityMetadataCollection.MetadataError.dAppDefinitionNotReciprocating
}

return self
}
}

Expand Down
29 changes: 5 additions & 24 deletions Sources/Clients/ROLAClient/ROLAClient+Live.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,34 +53,15 @@ extension ROLAClient {

return Self(
performDappDefinitionVerification: { metadata async throws in

let metadataCollection = try await cacheClient.withCaching(
_ = try await cacheClient.withCaching(
cacheEntry: .rolaDappVerificationMetadata(metadata.dAppDefinitionAddress.address),
request: {
try await gatewayAPIClient.getEntityMetadata(metadata.dAppDefinitionAddress.address, [.accountType, .relatedWebsites])
try await gatewayAPIClient.getDappMetadata(
metadata.dAppDefinitionAddress,
validatingWebsite: metadata.origin.url
)
}
)

let dict: [EntityMetadataKey: String] = .init(
uniqueKeysWithValues: metadataCollection.items.compactMap { item in
guard let key = EntityMetadataKey(rawValue: item.key),
let value = item.value.asString else { return nil }
return (key: key, value: value)
}
)

let dAppDefinitionMetadata = DappDefinitionMetadata(
accountType: dict[.accountType],
relatedWebsites: dict[.relatedWebsites]
)

guard dAppDefinitionMetadata.accountType == GatewayAPI.EntityMetadataCollection.AccountType.dappDefinition.rawValue else {
throw ROLAFailure.wrongAccountType
}

guard dAppDefinitionMetadata.relatedWebsites == metadata.origin.urlString.rawValue else {
throw ROLAFailure.unknownWebsite
}
},
performWellKnownFileCheck: { metadata async throws in
@Dependency(\.urlSession) var urlSession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ extension AccountList.Row {
.onTapGesture {
viewStore.send(.tapped)
}
.task { @MainActor in
.task {
await ViewStore(store.stateless).send(.view(.task)).finish()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI
// MARK: - DetailsContainerWithHeaderViewState
struct DetailsContainerWithHeaderViewState: Equatable {
let title: String
let amount: String
let amount: String?
let symbol: String?
}

Expand Down Expand Up @@ -61,11 +61,13 @@ struct DetailsContainerWithHeaderView<ThumbnailView: View, DetailsView: View>: V
VStack(spacing: .medium3) {
thumbnailView

Text(viewState.amount)
.font(.app.sheetTitle)
.kerning(-0.5)
+ Text((viewState.symbol).map { " " + $0 } ?? "")
.font(.app.sectionHeader)
if let amount = viewState.amount {
Text(amount)
.font(.app.sheetTitle)
.kerning(-0.5)
+ Text((viewState.symbol).map { " " + $0 } ?? "")
.font(.app.sectionHeader)
}
}
.padding(.vertical, .small2)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ public struct FungibleTokenDetails: Sendable, FeatureReducer {
public struct State: Sendable, Hashable {
let resource: AccountPortfolio.FungibleResource
let isXRD: Bool
let context: Context

public init(resource: AccountPortfolio.FungibleResource, isXRD: Bool) {
public init(resource: AccountPortfolio.FungibleResource, isXRD: Bool, context: Context) {
self.resource = resource
self.isXRD = isXRD
self.context = context
}

public enum Context: Equatable, Sendable {
case transfer
case portfolio
}
}

Expand Down
Loading
Loading