Jorge-Lopes / KREAd

KREAd is an application that uses and implements composable NFTs on Agoric, the application allows users to customize their own characters by equipping and unequipping items.
Apache License 2.0
0 stars 0 forks source link

Design Proposition For Performance Issue #3

Open anilhelvaci opened 7 months ago

anilhelvaci commented 7 months ago

Design Proposition For KREAd Performance Issue

Context

See https://github.com/Agoric/agoric-sdk/issues/8862

Problems targeted

Alternative Inventory

The current KREAd implementation uses multiple ZCFSeats to store equipped items and a duplicate of user's character
to ensure users can interact with the contract under Zoe's protection. What we propose has two main improvements to this implementation;

Implementation Approaches

Both approaches below takes the idea above and implements them with different considerations.

Migration

There are users who already minted characters and items with the existing way. We must implement a mechanism to migrate those users to the latest version. This means;

We should probably perform this migration as old users try to interact with the application after the upgrade instead of trying to move all characters and items at once(for the sake of Zoe). So we end two main ways to do this;

  1. Require a dedicated migration offer from the user if their character DOES NOT have a registered purse
    • Pros
      • By isolating migration process into a new offer, contract code complexity will probably be reduced
      • The usual interactions(equip, unequip, ..etc) will not take longer time
      • (If ITEMS issuer changes) We can structure the offer in the frontend in a way that users include their unequipped items in their migration offers so that we have a plan for how to get rid of assets that still use old ITEMS issuer.
    • Cons
      • Users will have to pay an additional transaction fee for one time
  2. Perform migration as a middleware operation once an action request from an old user is received(equip, unequip, ..etc).
    • Pros
      • Better UX
      • Less transaction fee for the user
    • Cons
      • More complexity in contract code
      • Harder to track block time for the usual operations(equip, unequip, ..etc) that include migration(we'll have manually look for in the usual operation includes any migration or not).

How to migrate characters/items that are on sale?

This question has its own heading simply because it's a pretty big one and we don't have a high level of confidence in the possible directions we're considering to take.

The KREAd market place has currently 524 items and 35 characters in sale. All sales are managed with an open ZCFSeat. There are two types of item sales, (1) First sale, means items minted by KREAd and being sold directly by the protocol, (2) Secondary sale, means users are selling the items they own. All character sales are done by the users, meaning they are similar to secondary item sales. From Zoe's point of view, it makes no difference whether the sale is secondary or not because all payments escrowed in various number of ZCFSeats are managed by the same purse. However during the migration, we might have to deal with first and secondary sales differently. The reason being, secondary sales actually have a user expecting a payout when first sales send the payout to some protocol account. Which means, in order not to break customer experience we must take careful approach in this. This problem has both a product and technical aspect to it. The product side is more related to the secondary sales whereas technical side is going to care about the first sales. This is simply because the majority of 524 items on sale are first sales. Therefore, most of the load is coming from there. The ideas we've considered so far for the first item sales;

Other Open Questions

anilhelvaci commented 7 months ago

Inventory Vat Approach

prepared by: @anilhelvaci

As stated in the problems targeted section, we are trying to relieve zoe from having to deal with KreadCharacter and KreadItem purses.

Assumptions

Solution

This approach presents a solution with the current features;

Diagram for operation mint with this solution

sequenceDiagram
    box Blue User
    participant Usw as SmartWallet
    end
    participant Zoe
    box Black KREAd
    participant Contract
    participant State
    end
    participant Zoe1 as Zoe
    box Green KREAdInventory
    participant Isw as KREAdInventoryContract
    end

    Isw ->> Isw: create KreadItem issuer kit
    Usw->>Zoe: mint offerSpec
    Zoe->>Contract: Proposal - give Price
    Zoe-->>Usw: UserSeat
    Contract->>Contract: create Character amount
    rect rgb(125, 0, 0)
    critical create Inventory Account
    Contract->>+Zoe1: registerCharacter offerSpec
    Zoe1-->>Contract: { userSeat, deposited }
    Zoe1 ->> Isw: registerCharacterHandler
    Isw ->> Isw: create purse for equipped items
    Isw ->> Isw: update character -> purse table
    Isw-->>Zoe1: mintItemsIntoPurseInvitation
    Zoe1-->>- Contract: offerResult<mintItemsIntoPurseInvitation>
    Contract ->> Contract: userSeat.getOfferResult()
    end
    end
    Contract->>Contract: mint Character
    rect rgb(79, 29, 2)
    critical create Inventory Account
    Contract->>+Zoe1: mintItemsIntoPurse offerSpec
    Zoe1-->>Contract: { userSeat, deposited }
    Zoe1 ->> Isw: mintItemsIntoPurseHandler
    Isw ->> Isw: find items purse for character
    Isw ->> Isw: mint defaultItems into the purse
    Isw-->>Zoe1: "Items successfully minted"
    Zoe1-->>- Contract: offerResult< "Items successfully minted">
    Contract ->> Contract: userSeat.getOfferResult()
    end
    end
    Contract->>Zoe: reallocate Character
    Contract->>Contract: exit user zcfSeat
    Zoe-->>Usw: Character payout
    Contract->>Contract: build new Character object (including the Inventory SW)
    Contract->>State: Add the new Character to Entries storage

Diagram for operation equip with this solution

sequenceDiagram
    box Blue User
    participant Usw as SmartWallet
    end
    participant Zoe
    box Black KREAd
    participant Contract
    participant State
    end
    participant Zoe1 as Zoe
    box Green KREAdInventory
    participant Isw as KREAdInventoryContract
    end

    Usw->>Zoe: equip offerSpec
    Zoe->>Contract: Proposal - { give CharacterIn + Item } { want CharacterOut }
    Zoe-->>Usw: UserSeat
    rect rgb(125, 0, 0)
    critical create Inventory Account
    Contract->>+Zoe1: equipItem offerSpec
    Zoe1-->>Contract: { userSeat, deposited }
    Zoe1 ->> Isw: equipItemHandler
    Isw ->> Isw: find items purse for character
    Isw ->> Isw: add new Item to the purse
    Isw-->>Zoe1: "Item successfully added"
    Zoe1-->>- Contract: offerResult<"Item successfully added">
    Contract ->> Contract: userSeat.getOfferResult()
    end
    end
    Contract -->> Zoe: "Item successfully added"
    Zoe-->>Usw: offerResult<"Item successfully added">
    Contract->>Contract: update storage with character and new items

Migration

There are users who already minted characters and items with the existing way. We must implement a mechanism to migrate those users to the latest version. This means;

We should probably perform this migration as old users try to interact with the application after the upgrade instead of trying to move all characters and items at once(for the sake of Zoe). So we end two main ways to do this;

  1. Require a new migration offer from the user if their character IS NOT registered in vat-inventory
    • Pros
      • By isolating migration process into a new offer contract code complexity will probably be reduced
      • The usual interactions(equip, unequip, ..etc) will not take longer time
    • Cons
      • Users will have to pay an additional transaction fee for one time
  2. Perform migration as a middleware operation once an action request from an old user is received(equip, unequip, ..etc).
    • Pros
      • Better UX
      • Less transaction fee for the user
    • Cons
      • More complexity in contract code
      • Harder to track block time for the usual operations(equip, unequip, ..etc) that include migration(we'll have manually look for in the usual operation includes any migration or not).

Open Questions

Considerations

anilhelvaci commented 6 months ago

In vat-kread Approach

Prepared by: @Jorge-Lopes

Since the purpose of the next diagrams are to display the changes we intend to do to the original design, we will omit some details of the normal flow of the KREAd features that are not relevant.

Mint Character

sequenceDiagram
    box Blue User
    participant Usw as SmartWallet
    end
    participant Zoe
    box Green KREAd
    participant kreadKit
    participant State
    end

    kreadKit ->> kreadKit: itemKit = makeIssuerKit()
    Usw->>Zoe: mint offerSpec
    Zoe->>kreadKit: Proposal { give Price }
    Zoe-->>Usw: UserSeat
    kreadKit->>kreadKit: characterMint.mintPayment(baseCharacter)
    kreadKit->>kreadKit: itemMint.mintPayment(baseItems)
    create participant Purse
    kreadKit->>Purse: itemsIssuer.makeEmptyPurse()
    Purse-->>kreadKit: provide inventoryPurse
    create participant Seat
    kreadKit->>Seat: zcf.makeEmptySeatKit()
    Seat-->>kreadKit: provide userSeat and zcfSeat
    kreadKit->>Zoe: transfer Character
    kreadKit->>Seat: transfer Items
    kreadKit->>Zoe: exit userSeat
    kreadKit->>Seat: exit inventorySeat
    kreadKit->>Seat: inventorySeat.getPayouts()
    destroy Seat
    Seat-->>kreadKit: return Items payment
    kreadKit->>Purse: deposit Items into Purse
    Zoe-->>Usw: Character payout
    kreadKit->>State: remove baseCharacter from character.base
    note over kreadKit: the new Character has the attribute Items purse
    kreadKit->>State: add the new Character to character.entries
    kreadKit->>State: remove baseItems from items.base
    kreadKit->>State: add Purse to items.entries

Equip Item

On this diagram, we can observe an inventorySeat being created, the reason for that is to ensure the offer safety required for this process. More specifically, we need to ensure that the user is providing the respective Item payment that was specified in the proposal.

sequenceDiagram
    box Blue User
    participant Usw as SmartWallet
    end
    participant Zoe
    box Green KREAd
    participant kreadKit
    participant State
    end

    Usw->>Zoe: equip offerSpec
    Zoe->>kreadKit: Proposal {give CharacterIn + Item } {want CharacterOut}
    Zoe-->>Usw: UserSeat
    kreadKit->>kreadKit: validate Character
    kreadKit->>State: get characterRecord from character.entries
    State-->>kreadKit: return characterRecord
    kreadKit->>State: get respective Purse from items.entries
    create participant Purse
    State->>Purse: get Purse
    Purse-->>kreadKit: provide inventoryPurse
    create participant Seat
    kreadKit->>Seat: zcf.makeEmptySeatKit()
    Seat-->>kreadKit: provide userSeat and zcfSeat
    kreadKit->>Zoe: transfer Character
    kreadKit->>Seat: transfer Items
    kreadKit->>Zoe: exit userSeat
    kreadKit->>Seat: exit inventorySeat
    kreadKit->>Seat: inventorySeat.getPayouts()
    destroy Seat
    Seat-->>kreadKit: return Items payment
    kreadKit->>Purse: deposit Items into Purse
    Zoe-->>Usw: Character payout

Unequip Item

sequenceDiagram
    box Blue User
    participant Usw as SmartWallet
    end
    participant Zoe
    box Green KREAd
    participant kreadKit
    participant State
    end

    Usw->>Zoe: unequip offerSpec
    Zoe->>kreadKit: Proposal {give CharacterIn} {want CharacterOut + Item}
    Zoe-->>Usw: UserSeat
    kreadKit->>kreadKit: validate Character
    kreadKit->>State: get characterRecord from character.entries
    State-->>kreadKit: return characterRecord
    create participant Purse
    kreadKit->>Purse: get respective Purse
    Purse-->>kreadKit: provide inventoryPurse
    kreadKit->>Purse: Purse.withdraw(Item)
    Purse-->>kreadKit: return Item payment
    kreadKit->>Zoe: transfer Character + Item
    kreadKit->>Zoe: exit userSeat
    Zoe-->>Usw: Character + Item payout

Sell Character

sequenceDiagram
    box Blue User
    participant Usw as SmartWallet
    end
    participant Zoe
    box Green KREAd
    participant kreadKit
    participant State
    end

    Usw->>Zoe: sellCharacter offerSpec
    Zoe->>kreadKit: Proposal {give Character} {want Price}
    Zoe-->>Usw: UserSeat
    kreadKit->>kreadKit: validate Character
    create participant Purse
    kreadKit->>Purse: itemsIssuer.makeEmptyPurse()
    Note right of kreadKit: Why purse from item?
    Purse-->>kreadKit: provide characterMarketPurse
    create participant Seat
    kreadKit->>Seat: zcf.makeEmptySeatKit()
    Seat-->>kreadKit: provide userSeat and zcfSeat
    kreadKit->>Seat: transfer Character
    kreadKit->>Seat: exit inventorySeat
    kreadKit->>Seat: inventorySeat.getPayouts()
    destroy Seat
    Seat-->>kreadKit: return payment
    kreadKit->>Purse: deposit Character into Purse
    note over kreadKit: the marketEntryGuard will include the Purse
    kreadKit->>State: add new sell entry to market.characterEntries

Buy Character

sequenceDiagram
    box Blue User
    participant Usw as SmartWallet
    end
    participant Zoe
    box Green KREAd
    participant kreadKit
    participant State
    end

    Usw->>Zoe: buyCharacter offerSpec
    Zoe->>kreadKit: Proposal {give Price} {want Character}
    Zoe-->>Usw: UserSeat
    kreadKit->>kreadKit: validate Character
    kreadKit->>State: get sell record from market.characterEntries
    State-->>kreadKit: return sell record
    create participant Purse
    kreadKit->>Purse: get respective Purse
    Purse-->>kreadKit: provide characterMarketPurse
    kreadKit->>Purse: purse.withdraw(Character)
    Purse-->>kreadKit: return Character payment
    create participant Seat
    kreadKit->>Seat: get respective seller seat
    Seat-->>kreadKit: provide userSeat
    kreadKit->>Seat: transfer Price
    kreadKit->>Zoe: transfer Character
    destroy Seat
    kreadKit->>Seat: exit sellerSeat
    kreadKit->>Zoe: exit userSeat
    kreadKit->>State: remove sellRecord to market.characterEntries

Contract state

The major difference in the contact state is the inventory Purse that was included into the CharacterEntryGuard

classDiagram

baggage *-- contractState
contractState *-- charactersState
contractState *-- itemsState
contractState *-- marketState
charactersState *-- characters
charactersState *-- baseCharacters
itemsState *-- items
itemsState *-- baseItems
marketState *-- characterMarket
marketState *-- itemMarket
marketState *-- marketMetrics

baseCharacters o-- BaseCharacterGuard
characters o-- CharacterEntryGuard
items o-- ItemRecorderGuard
baseItems o-- ItemGuard
characterMarket o-- MarketEntryGuard
itemMarket o-- MarketEntryGuard
marketMetrics o-- MarketMetricsGuard
CharacterEntryGuard o-- HistoryGuard
ItemRecorderGuard o-- HistoryGuard
CharacterEntryGuard o-- CharacterGuard
ItemRecorderGuard o-- ItemGuard
MarketEntryGuard o-- CharacterGuard
MarketEntryGuard o-- ItemGuard

class characters {
    +String keyShape
    +CharacterEntryGuard valueShape
}

class baseCharacters {
    +Number keyShape
    +BaseCharacterGuard valueShape
}

class items {
    +Number keyShape
    +ItemRecorderGuard valueShape
}

class baseItems {
    +String keyShape
    +List~ItemGuard~ valueShape
}

class characterMarket {
    +String keyShape
    +MarketEntryGuard valueShape
}

class itemMarket {
    +Number keyShape
    +MarketEntryGuard valueShape
}

class marketMetrics {
    +String keyShape
    +MarketMetricsGuard valueShape
}

class BaseCharacterGuard {
    +String title
    +String description
    +String origin
    +Number level
    +String artistMetadata
    +String image
    +String characterTraits
}

class CharacterEntryGuard {
    +String name
    +CharacterGuard character
    +Purse inventory
    +RecorderKit inventoryKit
    +List~HistoryGuard~ history
}

class ItemRecorderGuard {
    +Number id
    +ItemGuard item
    +List~HistoryGuard~ history
}

class MarketEntryGuard {
    +String or Number id
    +Seat seat
    +Purse purse
    +RecorderKit recorderKit
    +Amount askingPrice
    +Amount royalty
    +Amount platformFee
    +CharacterGuard or ItemGuard asset
    +bool isFirstSale
}

class MarketMetricsGuard {
  +Number amountSold
  +Number collectionSize
  +Number averageLevel
  +Number marketplaceAverageLevel
  +Number latestSalePrice
  +Number putForSaleCount
}

class HistoryGuard {
    +String type
    +Any data
    +Timestamp timestamp
}

class CharacterGuard {
    +String title
    +String description
    +String origin
    +Number level
    +String artistMetadata
    +String image
    +String characterTraits
    +String name
    +Number keyId
    +Number id
    +timestamp date
}

class ItemGuard {
    +String name
    +String category
    +String description
    +bool functional
    +String origin
    +String image
    +String thumbnail
    +Number rarity
    +Number level
    +Number filtering
    +Number weight
    +Number sense
    +Number reserves
    +Number durability
    +String colors
    +List~String~ artistMetadata
}