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

Third party asset deposit rule - local #679

Merged
merged 35 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0ce633e
add children folder
GhenadieVP Aug 17, 2023
f1e21e1
generic section
GhenadieVP Aug 18, 2023
aaff5ac
wip
GhenadieVP Aug 18, 2023
d362d04
wip
GhenadieVP Aug 18, 2023
66f96db
wip
GhenadieVP Aug 19, 2023
e319a33
wip
GhenadieVP Aug 19, 2023
4abc53e
wip
GhenadieVP Aug 19, 2023
24fd0a7
wip
GhenadieVP Aug 19, 2023
37d74ff
add asset ui adjustements
GhenadieVP Aug 19, 2023
ecd990c
add nft id
GhenadieVP Aug 19, 2023
ff6aede
reusable resources list
GhenadieVP Aug 19, 2023
d9c8459
wip
GhenadieVP Aug 20, 2023
e37f009
profile update
GhenadieVP Aug 20, 2023
328882b
update tests
GhenadieVP Aug 20, 2023
5179494
Merge branch 'third_party_deposits_profile' into fix/ABW-1766-i-os-th…
GhenadieVP Aug 20, 2023
19bf83e
link with profile
GhenadieVP Aug 20, 2023
5e48b17
Prepare for new client
GhenadieVP Aug 20, 2023
6d1bb7d
onLedgerclient
GhenadieVP Aug 21, 2023
fab08df
impr
GhenadieVP Aug 21, 2023
e30afc0
Merge branch 'third_party_deposits_profile' into fix/ABW-1766-i-os-th…
GhenadieVP Aug 21, 2023
eb29f7c
client
GhenadieVP Aug 22, 2023
dd6b5bd
wip
GhenadieVP Aug 22, 2023
86308e5
improve
GhenadieVP Aug 22, 2023
230acd2
asset exception can be only resource address
GhenadieVP Aug 22, 2023
c954ad3
format
GhenadieVP Aug 22, 2023
c26d9db
Merge branch 'profile-thirdPartyDeposit-fix' into fix/ABW-1766-i-os-t…
GhenadieVP Aug 22, 2023
2ff78c9
wip
GhenadieVP Aug 22, 2023
36f9db1
impr
GhenadieVP Aug 23, 2023
77ff217
wip
GhenadieVP Aug 23, 2023
3fda42e
wip
GhenadieVP Aug 23, 2023
0ba7aa7
Merge branch 'main' into fix/ABW-1766-i-os-third-party-asset-deposit-…
GhenadieVP Aug 23, 2023
b35ab89
wip
GhenadieVP Aug 23, 2023
44dddd2
wip
GhenadieVP Aug 23, 2023
e365666
Merge branch 'main' into fix/ABW-1766-i-os-third-party-asset-deposit-…
GhenadieVP Aug 23, 2023
fee0793
review followup
GhenadieVP Aug 24, 2023
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
10 changes: 10 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ package.addModules([
"CreateAuthKeyFeature",
"ShowQRFeature",
"OverlayWindowClient",
"OnLedgerEntitiesClient",
],
tests: .yes()
),
Expand Down Expand Up @@ -464,6 +465,15 @@ package.addModules([
],
tests: .yes()
),
.client(
name: "OnLedgerEntitiesClient",
dependencies: [
"GatewayAPI",
"CacheClient",
"EngineKit",
],
tests: .no
),
.client(
name: "AppPreferencesClient",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,11 @@ extension AccountPortfoliosClient {
_ rawAccountDetails: GatewayAPI.StateEntityDetailsResponseItem,
ledgerState: GatewayAPI.LedgerState
) async throws -> AccountPortfolio {
@Dependency(\.gatewayAPIClient) var gatewayAPIClient

let (rawFungibleResources, rawNonFungibleResources) = try await (
fetchAllFungibleResources(rawAccountDetails, ledgerState: ledgerState),
fetchAllNonFungibleResources(rawAccountDetails, ledgerState: ledgerState)
gatewayAPIClient.fetchAllFungibleResources(rawAccountDetails, ledgerState: ledgerState),
gatewayAPIClient.fetchAllNonFungibleResources(rawAccountDetails, ledgerState: ledgerState)
)

let poolUnitResources = try await createPoolUnitResources(
Expand Down Expand Up @@ -215,8 +217,8 @@ extension AccountPortfoliosClient {

let resourceAddress = try ResourceAddress(validatingAddress: resource.resourceAddress)
let divisibility = details?.divisibility
let behaviors = (details?.roleAssignments).map(extractBehaviors) ?? []
let tags = extractTags(item: item)
let behaviors = details?.roleAssignments.extractBehaviors() ?? []
let tags = item.extractTags()
let totalSupply = details.flatMap { try? BigDecimal(fromString: $0.totalSupply) }
let metadata = resource.explicitMetadata

Expand Down Expand Up @@ -266,9 +268,9 @@ extension AccountPortfoliosClient {
return firstPageItems
}

let additionalItems = try await fetchAllPaginatedItems(
cursor: PageCursor(ledgerState: ledgerState, nextPageCursor: nextPageCursor),
fetchEntityNonFungibleResourceIdsPage(
let additionalItems = try await gatewayAPIClient.fetchAllPaginatedItems(
cursor: GatewayAPIClient.PageCursor(ledgerState: ledgerState, nextPageCursor: nextPageCursor),
gatewayAPIClient.fetchEntityNonFungibleResourceIdsPage(
accountAddress,
resourceAddress: resource.resourceAddress,
vaultAddress: vault.vaultAddress
Expand Down Expand Up @@ -321,8 +323,8 @@ extension AccountPortfoliosClient {
let item = try await gatewayAPIClient.getSingleEntityDetails(resource.resourceAddress)
let details = item.details?.nonFungible

let behaviors = (details?.roleAssignments).map(extractBehaviors) ?? []
let tags = extractTags(item: item)
let behaviors = details?.roleAssignments.extractBehaviors() ?? []
let tags = item.extractTags()
let totalSupply = details.flatMap { try? BigDecimal(fromString: $0.totalSupply) }

// Load the nftIds from the resource vault
Expand Down Expand Up @@ -507,7 +509,7 @@ extension AccountPortfoliosClient {
return nil
}

let allFungibleResources = try await fetchAllFungibleResources(item, ledgerState: ledgerState)
let allFungibleResources = try await gatewayAPIClient.fetchAllFungibleResources(item, ledgerState: ledgerState)

guard !allFungibleResources.isEmpty else {
assertionFailure("Empty Pool Unit!!!")
Expand Down Expand Up @@ -535,279 +537,6 @@ extension AccountPortfoliosClient {
}

// MARK: - Endpoints
extension AccountPortfoliosClient {
@Sendable
static func fetchAllFungibleResources(
_ entityDetails: GatewayAPI.StateEntityDetailsResponseItem,
ledgerState: GatewayAPI.LedgerState
) async throws -> [GatewayAPI.FungibleResourcesCollectionItem] {
guard let firstPage = entityDetails.fungibleResources else {
return [GatewayAPI.FungibleResourcesCollectionItem]()
}

guard let nextPageCursor = firstPage.nextCursor else {
return firstPage.items
}

let additionalItems = try await fetchAllPaginatedItems(
cursor: PageCursor(ledgerState: ledgerState, nextPageCursor: nextPageCursor),
fetchFungibleResourcePage(entityDetails.address)
)

return firstPage.items + additionalItems
}

// FIXME: Similar function to the above, maybe worth extracting in a single function?
@Sendable
static func fetchAllNonFungibleResources(
_ entityDetails: GatewayAPI.StateEntityDetailsResponseItem,
ledgerState: GatewayAPI.LedgerState
) async throws -> [GatewayAPI.NonFungibleResourcesCollectionItem] {
guard let firstPage = entityDetails.nonFungibleResources else {
return [GatewayAPI.NonFungibleResourcesCollectionItem]()
}

guard let nextPageCursor = firstPage.nextCursor else {
return firstPage.items
}

let additionalItems = try await fetchAllPaginatedItems(
cursor: PageCursor(ledgerState: ledgerState, nextPageCursor: nextPageCursor),
fetchNonFungibleResourcePage(entityDetails.address)
)

return firstPage.items + additionalItems
}

static func fetchFungibleResourcePage(
_ entityAddress: String
) -> @Sendable (PageCursor?) async throws -> PaginatedResourceResponse<GatewayAPI.FungibleResourcesCollectionItem> {
@Dependency(\.gatewayAPIClient) var gatewayAPIClient

return { pageCursor in
let request = GatewayAPI.StateEntityFungiblesPageRequest(
atLedgerState: pageCursor?.ledgerState.selector,
cursor: pageCursor?.nextPageCursor,
address: entityAddress,
aggregationLevel: .vault
)
let response = try await gatewayAPIClient.getEntityFungiblesPage(request)

return .init(
loadedItems: response.items,
totalCount: response.totalCount,
cursor: response.nextCursor.map {
PageCursor(ledgerState: response.ledgerState, nextPageCursor: $0)
}
)
}
}

static func fetchNonFungibleResourcePage(
_ accountAddress: String
) -> @Sendable (PageCursor?) async throws -> PaginatedResourceResponse<GatewayAPI.NonFungibleResourcesCollectionItem> {
@Dependency(\.gatewayAPIClient) var gatewayAPIClient

return { pageCursor in
let request = GatewayAPI.StateEntityNonFungiblesPageRequest(
atLedgerState: pageCursor?.ledgerState.selector,
cursor: pageCursor?.nextPageCursor,
address: accountAddress,
aggregationLevel: .vault
)
let response = try await gatewayAPIClient.getEntityNonFungiblesPage(request)

return .init(
loadedItems: response.items,
totalCount: response.totalCount,
cursor: response.nextCursor.map {
PageCursor(ledgerState: response.ledgerState, nextPageCursor: $0)
}
)
}
}

static func fetchEntityNonFungibleResourceIdsPage(
_ accountAddress: String,
resourceAddress: String,
vaultAddress: String
) -> @Sendable (PageCursor?) async throws -> PaginatedResourceResponse<String> {
@Dependency(\.gatewayAPIClient) var gatewayAPIClient

return { pageCursor in
let request = GatewayAPI.StateEntityNonFungibleIdsPageRequest(
atLedgerState: pageCursor?.ledgerState.selector,
cursor: pageCursor?.nextPageCursor,
address: accountAddress,
vaultAddress: vaultAddress,
resourceAddress: resourceAddress
)
let response = try await gatewayAPIClient.getEntityNonFungibleIdsPage(request)

return .init(
loadedItems: response.items,
totalCount: response.totalCount,
cursor: response.nextCursor.map {
PageCursor(ledgerState: response.ledgerState, nextPageCursor: $0)
}
)
}
}
}

// MARK: - Pagination
extension AccountPortfoliosClient {
/// A page cursor is required to have the `nextPageCurosr` itself, as well the `ledgerState` of the previous page.
struct PageCursor: Hashable, Sendable {
let ledgerState: GatewayAPI.LedgerState
let nextPageCursor: String
}

struct PaginatedResourceResponse<Resource: Sendable>: Sendable {
let loadedItems: [Resource]
let totalCount: Int64?
let cursor: PageCursor?
}

/// Recursively fetches all of the pages for a given paginated request.
///
/// Provide an initial page cursor if needed to load the all the items starting with a given page
@Sendable
static func fetchAllPaginatedItems<Item>(
cursor: PageCursor?,
_ paginatedRequest: @Sendable @escaping (_ cursor: PageCursor?) async throws -> PaginatedResourceResponse<Item>
) async throws -> [Item] {
@Sendable
func fetchAllPaginatedItems(
collectedResources: PaginatedResourceResponse<Item>?
) async throws -> [Item] {
/// Finish when some items where loaded and the nextPageCursor is nil.
if let collectedResources, collectedResources.cursor == nil {
return collectedResources.loadedItems
}

/// We can request here with nil nextPageCursor, as the first page will not have a cursor.
let response = try await paginatedRequest(collectedResources?.cursor)
let oldItems = collectedResources?.loadedItems ?? []
let allItems = oldItems + response.loadedItems

let nextPageCursor: PageCursor? = {
// Safeguard: Don't rely only on the gateway returning nil for the next page cursor,
// if happened to load an empty page, or all items were loaded - next page cursor is nil.
if response.loadedItems.isEmpty || allItems.count == response.totalCount.map(Int.init) {
return nil
}

return response.cursor
}()

let result = PaginatedResourceResponse(loadedItems: allItems, totalCount: response.totalCount, cursor: nextPageCursor)
return try await fetchAllPaginatedItems(collectedResources: result)
}

return try await fetchAllPaginatedItems(
collectedResources: cursor.map {
PaginatedResourceResponse(loadedItems: [], totalCount: nil, cursor: $0)
}
)
}
}

extension AccountPortfoliosClient {
@Sendable static func extractBehaviors(assignments: GatewayAPI.ComponentEntityRoleAssignments) -> [AssetBehavior] {
typealias ParsedName = GatewayAPI.RoleKey.ParsedName

enum Assigned {
case none, someone, anyone, unknown
}

func findEntry(_ name: GatewayAPI.RoleKey.ParsedName) -> GatewayAPI.ComponentEntityRoleAssignmentEntry? {
assignments.entries.first { $0.roleKey.parsedName == name }
}

func performer(_ name: GatewayAPI.RoleKey.ParsedName) -> Assigned {
guard let assignment = findEntry(name)?.parsedAssignment else { return .unknown }
switch assignment {
case .allowAll: return .anyone
case .denyAll: return .none
case .protected, .otherExplicit, .owner: return .someone
}
}

func updaters(_ name: GatewayAPI.RoleKey.ParsedName) -> Assigned {
guard let updaters = findEntry(name)?.updaterRoles, !updaters.isEmpty else { return .none }

// Lookup the corresponding assignments, ignoring unknown and empty values
let updaterAssignments = Set(updaters.compactMap(\.parsedName).compactMap(findEntry).compactMap(\.parsedAssignment))

if updaterAssignments.isEmpty {
return .unknown
} else if updaterAssignments == [.denyAll] {
return .none
} else if updaterAssignments.contains(.allowAll) {
return .anyone
} else {
return .someone
}
}

var result: Set<AssetBehavior> = []

// Withdrawer and depositor areas are checked together, but we look at the performer and updater role types separately
let movers: Set = [performer(.withdrawer), performer(.depositor)]
if movers != [.anyone] {
result.insert(.movementRestricted)
} else {
let moverUpdaters: Set = [updaters(.withdrawer), updaters(.depositor)]
if moverUpdaters.contains(.anyone) {
result.insert(.movementRestrictableInFutureByAnyone)
} else if moverUpdaters.contains(.someone) {
result.insert(.movementRestrictableInFuture)
}
}

// Other names are checked individually, but without distinguishing between the role types
func addBehavior(for name: GatewayAPI.RoleKey.ParsedName, ifSomeone: AssetBehavior, ifAnyone: AssetBehavior) {
let either: Set = [performer(name), updaters(name)]
if either.contains(.anyone) {
result.insert(ifAnyone)
} else if either.contains(.someone) {
result.insert(ifSomeone)
}
}

addBehavior(for: .minter, ifSomeone: .supplyIncreasable, ifAnyone: .supplyIncreasableByAnyone)
addBehavior(for: .burner, ifSomeone: .supplyDecreasable, ifAnyone: .supplyDecreasableByAnyone)
addBehavior(for: .recaller, ifSomeone: .removableByThirdParty, ifAnyone: .removableByAnyone)
addBehavior(for: .freezer, ifSomeone: .freezableByThirdParty, ifAnyone: .freezableByAnyone)
addBehavior(for: .nonFungibleDataUpdater, ifSomeone: .nftDataChangeable, ifAnyone: .nftDataChangeableByAnyone)

// If there are no special behaviors, that means it's a "simple asset"
if result.isEmpty {
return [.simpleAsset]
}

// Finally we make some simplifying substitutions
func substitute(_ source: Set<AssetBehavior>, with target: AssetBehavior) {
if result.isSuperset(of: source) {
result.subtract(source)
result.insert(target)
}
}

// If supply is both increasable and decreasable, then it's "flexible"
substitute([.supplyIncreasableByAnyone, .supplyDecreasableByAnyone], with: .supplyFlexibleByAnyone)
substitute([.supplyIncreasable, .supplyDecreasable], with: .supplyFlexible)

return result.sorted()
}
}

extension AccountPortfoliosClient {
@Sendable static func extractTags(item: GatewayAPI.StateEntityDetailsResponseItem) -> [AssetTag] {
item.metadata.tags?.compactMap(NonEmptyString.init(rawValue:)).map(AssetTag.init) ?? []
}
}

extension Array where Element == AccountPortfolio.FungibleResource {
func sorted() async -> AccountPortfolio.FungibleResources {
Expand Down Expand Up @@ -884,7 +613,6 @@ extension AccountPortfolio.PoolUnitResources {
}
}

// FIXME: Temporary hack to extract the key_image_url, until we have a proper schema
extension GatewayAPI.StateNonFungibleDetailsResponseItem {
public typealias NFTData = AccountPortfolio.NonFungibleResource.NonFungibleToken.NFTData
public var details: [NFTData] {
Expand Down
Loading