From a94671ddc5cd20be07a4c0461682e3b75a1e0f16 Mon Sep 17 00:00:00 2001 From: Pandelis Symeonidis <54764628+Pandelissym@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:57:33 +0100 Subject: [PATCH] Frontend MakeOffer callback pattern (#104) ## Description In this PR: Created a new pattern for creating offer callbacks that get passed the the wallet's `makeOffer` function. The pattern defined in `contract-callbacks.ts` allows developers to define their callback functions in the view components themselves. Also, having passed the callback from the view component to the hooks you are able to overwrite the callback and add common logic to them (see for example `sevice/character.ts` `useBuyCharacter` hook). ``` interface MakeOfferCallback { accepted?: (args?: any) => void, // offer was successful refunded?: (args?: any) => void, // strangely seems to behave the same way as accepted error?: (args?: any) => void, // offer failed seated?: (args?: any) => void, // returned exclusively by the KREAd sell method, likely has to do with the offer being long-lived settled?: (args?: any) => void, // gets called when a response is received, regardless of the status setIsLoading?: React.Dispatch> }; ``` You can now implement this interface in a view component passing it callbacks for specific cases (error, accepted, etc). This object is passed from the view to the hook. There common functionality can be added to the callback by doing so: ``` { ...callback, accepted: () => { if (callback.accepted) callback.accepted(); // common functionality }, }, ``` This object is then passed to the service functions where it is passed through the `formOfferResultCallback` function that generates the complete callback that the agoric `makeOffer` function expects. ## Checklist Make sure all items are checked before submitting the pull request. Remove any items that are not applicable. - [x] I have used agoric's linter on my code (https://github.com/Agoric/agoric-sdk/discussions/8274) - [x] I have updated the documentation to reflect the changes (if applicable). - [x] I have added/updated unit tests to cover the changes. - [x] All existing tests pass. - [x] The PR title is clear and concise. - [x] Are there changes in the /fronted folder? Make sure `cd frontend && yarn build` runs successfully.; --------- Co-authored-by: CARLOS TRIGO --- .prettierignore | 1 + frontend/package.json | 2 +- .../asset-card/item-card-inventory.tsx | 4 +- .../asset-details/item-details-inventory.tsx | 4 +- frontend/src/components/index.ts | 2 - .../src/components/menu-card/menu-card.tsx | 18 ++-- .../canvas/items-mode/items-mode.tsx | 5 +- .../detail-section-items.tsx | 2 +- frontend/src/interfaces/agoric.interfaces.ts | 21 ++++- .../src/interfaces/character.interfaces.ts | 1 - frontend/src/pages/buy/character-buy.tsx | 2 +- frontend/src/pages/buy/item-buy.tsx | 13 ++- .../create-character/create-character.tsx | 22 +++-- frontend/src/pages/landing/landing.tsx | 5 +- frontend/src/pages/sell/character-sell.tsx | 13 ++- frontend/src/pages/sell/item-sell.tsx | 8 +- frontend/src/service/character.ts | 90 +++++++++---------- frontend/src/service/character/inventory.ts | 75 ++++------------ frontend/src/service/character/market.ts | 72 +++------------ frontend/src/service/character/mint.ts | 22 ++--- frontend/src/service/items.ts | 87 ++++++++---------- frontend/src/util/contract-callbacks.ts | 30 +++++++ 22 files changed, 234 insertions(+), 265 deletions(-) create mode 100644 frontend/src/util/contract-callbacks.ts diff --git a/.prettierignore b/.prettierignore index 2e3981a3c..209efa215 100644 --- a/.prettierignore +++ b/.prettierignore @@ -26,3 +26,4 @@ /.pnp.js .env* +frontend/** \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 61bce9665..03d9bf632 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -102,5 +102,5 @@ "vite-plugin-svgr": "^2.2.1", "vite-tsconfig-paths": "^3.5.0" }, - "packageManager": "yarn@4.0.0" + "packageManager": "yarn@1.22.19" } diff --git a/frontend/src/components/asset-card/item-card-inventory.tsx b/frontend/src/components/asset-card/item-card-inventory.tsx index 3a3cdcf3e..888bcef13 100644 --- a/frontend/src/components/asset-card/item-card-inventory.tsx +++ b/frontend/src/components/asset-card/item-card-inventory.tsx @@ -41,13 +41,13 @@ export const ItemCardInventory: FC = ({ item, selectItem }) => { const equipAsset = (event: React.MouseEvent) => { event.stopPropagation(); setShowToast(true); - equipItem.mutate({ item }); + equipItem.mutate({ item, callback: {} }); }; const unequipAsset = (event: React.MouseEvent) => { event.stopPropagation(); setShowToast(true); - unequipItem.mutate({ item }); + unequipItem.mutate({ item, callback: {} }); }; const sellAsset = (event: React.MouseEvent) => { diff --git a/frontend/src/components/asset-details/item-details-inventory.tsx b/frontend/src/components/asset-details/item-details-inventory.tsx index 56e790af2..e15305940 100644 --- a/frontend/src/components/asset-details/item-details-inventory.tsx +++ b/frontend/src/components/asset-details/item-details-inventory.tsx @@ -32,12 +32,12 @@ export const ItemDetailsInventory: FC = ({ item, sele if (equipItem.isError || unequipItem.isError) return ; const equipAsset = () => { setShowToast(!showToast); - equipItem.mutate({ item }); + equipItem.mutate({ item, callback: {} }); }; const unequipAsset = () => { setShowToast(!showToast); - unequipItem.mutate({ item }); + unequipItem.mutate({ item, callback: {} }); }; const sellAsset = () => { diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 40ee36ae0..08a89d126 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -7,8 +7,6 @@ export * from "./item-card"; export * from "./input-fields"; export * from "./switch-selector"; export * from "./navigation-tab"; -export * from "./menu-card"; -export * from "./menu-item"; export * from "./price-in-ist"; export * from "./base-route"; export * from "./equipped-item-card"; diff --git a/frontend/src/components/menu-card/menu-card.tsx b/frontend/src/components/menu-card/menu-card.tsx index 699312f6a..dd7b89aec 100644 --- a/frontend/src/components/menu-card/menu-card.tsx +++ b/frontend/src/components/menu-card/menu-card.tsx @@ -1,5 +1,5 @@ import { FC, useMemo, useState } from "react"; -import { Item, Category } from "../../interfaces"; +import { Item, Category, MakeOfferCallback } from "../../interfaces"; import { text } from "../../assets"; import { ArrowContainer, @@ -44,8 +44,16 @@ export const MenuCard: FC = ({ title, category, equippedItemProp, const [showToast, setShowToast] = useState(false); const [equippedItem, setEquippedItem] = useState(equippedItemProp); - const equipItem = useEquipItem(setEquippedItem); - const unequipItem = useUnequipItem(() => setEquippedItem(undefined)); + const equipItem = useEquipItem(); + const unequipItem = useUnequipItem(); + + const handleEquipResult: MakeOfferCallback = { + accepted: setEquippedItem, + }; + + const handleUnequipResult: MakeOfferCallback = { + accepted: () => setEquippedItem(undefined), + }; const allItems = useMemo(() => { if (equippedItem) return [equippedItem, ...unequippedItems]; @@ -58,14 +66,14 @@ export const MenuCard: FC = ({ title, category, equippedItemProp, event.stopPropagation(); setShowToast(!showToast); if (!selectedItem) return; - equipItem.mutate({ item: selectedItem }); + equipItem.mutate({ item: selectedItem, callback: handleEquipResult }); }; const unequip = (event: React.MouseEvent) => { event.stopPropagation(); setShowToast(!showToast); if (!equippedItem) return; - unequipItem.mutate({ item: equippedItem }); + unequipItem.mutate({ item: equippedItem, callback: handleUnequipResult }); }; const primaryActions = () => { diff --git a/frontend/src/containers/canvas/items-mode/items-mode.tsx b/frontend/src/containers/canvas/items-mode/items-mode.tsx index d33738d45..f071e215e 100644 --- a/frontend/src/containers/canvas/items-mode/items-mode.tsx +++ b/frontend/src/containers/canvas/items-mode/items-mode.tsx @@ -58,12 +58,13 @@ export const ItemsMode: FC = () => { equipItem.mutate({ item: selected, currentlyEquipped: equipped.inCategory, + callback: {}, }); } setOnAssetChange(false); setShowToast(!showToast); if (!equipped.inCategory && selected) { - equipItem.mutate({ item: selected }); + equipItem.mutate({ item: selected, callback: {} }); } }; @@ -72,7 +73,7 @@ export const ItemsMode: FC = () => { setOnAssetChange(false); setShowToast(!showToast); if (equipped.inCategory) { - unequipItem.mutate({ item: equipped.inCategory }); + unequipItem.mutate({ item: equipped.inCategory, callback: {} }); } }; diff --git a/frontend/src/containers/detail-section/detail-section-items/detail-section-items.tsx b/frontend/src/containers/detail-section/detail-section-items/detail-section-items.tsx index 332ac4487..3f4e6ed57 100644 --- a/frontend/src/containers/detail-section/detail-section-items/detail-section-items.tsx +++ b/frontend/src/containers/detail-section/detail-section-items/detail-section-items.tsx @@ -32,7 +32,7 @@ const ListItem: FC = ({ item, showToast }) => { const unequip = () => { showToast(); - unequipItem.mutate({ item }); + unequipItem.mutate({ item, callback: {} }); }; return ( diff --git a/frontend/src/interfaces/agoric.interfaces.ts b/frontend/src/interfaces/agoric.interfaces.ts index d15937ae4..4a5c0ea93 100644 --- a/frontend/src/interfaces/agoric.interfaces.ts +++ b/frontend/src/interfaces/agoric.interfaces.ts @@ -1,7 +1,7 @@ interface Contracts { kread: { instance: any; - } + }; } interface Status { @@ -65,7 +65,6 @@ interface UpdateStatus { payload: { [key: string]: boolean }; } - interface SetOffers { type: "SET_OFFERS"; payload: any[]; @@ -136,3 +135,21 @@ export interface OfferProposal { give: any; want: any; } + +export const OFFER_STATUS = { + error: "error", + refunded: "refunded", + accepted: "accepted", + seated: "seated" +}; +export type OfferStatusType = keyof typeof OFFER_STATUS + +export interface MakeOfferCallback { + accepted?: (args?: any) => void, // offer was successful + refunded?: (args?: any) => void, // strangely seems to behave the same way as accepted + error?: (args?: any) => void, // offer failed + seated?: (args?: any) => void, // returned exclusively by the KREAd sell method, likely has to do with the offer being long-lived + settled?: (args?: any) => void, // gets called when a response is received, regardless of the status + setIsLoading?: React.Dispatch> +}; + diff --git a/frontend/src/interfaces/character.interfaces.ts b/frontend/src/interfaces/character.interfaces.ts index f343628ac..765aac7f4 100644 --- a/frontend/src/interfaces/character.interfaces.ts +++ b/frontend/src/interfaces/character.interfaces.ts @@ -99,5 +99,4 @@ export interface CharacterInMarketBackend { export interface CharacterCreation { name: string; - setError?: (error: string) => void } diff --git a/frontend/src/pages/buy/character-buy.tsx b/frontend/src/pages/buy/character-buy.tsx index 98f3349ee..694a03778 100644 --- a/frontend/src/pages/buy/character-buy.tsx +++ b/frontend/src/pages/buy/character-buy.tsx @@ -32,7 +32,7 @@ export const CharacterBuy = () => { const handleSubmit = async () => { if (!id) return; setIsAwaitingApproval(true); - await buyCharacter.callback(); + await buyCharacter.sendOffer({}); }; if (isLoadingCharacter) return ; diff --git a/frontend/src/pages/buy/item-buy.tsx b/frontend/src/pages/buy/item-buy.tsx index 17cb47d3f..0d65cf233 100644 --- a/frontend/src/pages/buy/item-buy.tsx +++ b/frontend/src/pages/buy/item-buy.tsx @@ -29,9 +29,16 @@ export const ItemBuy = () => { const handleSubmit = async () => { setIsAwaitingApproval(true); - await buyItem.callback(() => { - setIsOfferAccepted(true); - setIsAwaitingApproval(false); + await buyItem.sendOffer({ + refunded: () => { + setIsOfferAccepted(true); + }, + accepted: () => { + setIsOfferAccepted(true); + }, + settled: () => { + setIsAwaitingApproval(false); + } }); }; diff --git a/frontend/src/pages/create-character/create-character.tsx b/frontend/src/pages/create-character/create-character.tsx index 5360254b4..9f9b24a93 100644 --- a/frontend/src/pages/create-character/create-character.tsx +++ b/frontend/src/pages/create-character/create-character.tsx @@ -4,7 +4,7 @@ import { ErrorView, FadeInOut, FormHeader, LoadingPage, NotificationDetail, Over import { PageContainer } from "../../components/page-container"; import { MINTING_COST, MINT_CHARACTER_FLOW_STEPS, WALLET_INTERACTION_STEP } from "../../constants"; import { useIsMobile, useViewport } from "../../hooks"; -import { Character, CharacterCreation } from "../../interfaces"; +import { Character, CharacterCreation, MakeOfferCallback } from "../../interfaces"; import { routes } from "../../navigation"; import { useCreateCharacter } from "../../service"; import { Confirmation } from "./confirmation"; @@ -33,8 +33,8 @@ export const CreateCharacter: FC = () => { const mobile = useIsMobile(breakpoints.desktop); const { ist } = useWalletState(); - const notEnoughIST = useMemo(()=>{ - if(ist < MINTING_COST || !ist) { + const notEnoughIST = useMemo(() => { + if (ist < MINTING_COST || !ist) { return true; } return false; @@ -53,14 +53,24 @@ export const CreateCharacter: FC = () => { setCurrentStep(step); }; - const handleError = (error: string) => { + const errorCallback = (error: string) => { setError(error); setShowToast(true); - } + }; + const handleResult: MakeOfferCallback = { + error: errorCallback, + accepted: () => { + console.info("MintCharacter call settled"); + } + }; + const sendOfferHandler = async (): Promise => { setIsLoading(true); - await createCharacter.mutateAsync({ name: characterData.name, setError: handleError }); + await createCharacter.mutateAsync({ + name: characterData.name, + callback: handleResult, + }); }; const setData = async (data: CharacterCreation): Promise => { diff --git a/frontend/src/pages/landing/landing.tsx b/frontend/src/pages/landing/landing.tsx index deca53d1b..7140cafd4 100644 --- a/frontend/src/pages/landing/landing.tsx +++ b/frontend/src/pages/landing/landing.tsx @@ -14,7 +14,6 @@ import { Overlay, OverviewEmpty, PageSubTitle, - PageTitle, SecondaryButton, } from "../../components"; import { ButtonContainer, CharacterCardWrapper, DetailContainer, ItemCardWrapper } from "./styles"; @@ -64,14 +63,14 @@ export const Landing: FC = () => { const equipAsset = () => { setShowToast(!showToast); if (item) { - equipItem.mutate({ item }); + equipItem.mutate({ item, callback: {} }); } }; const unequipAsset = () => { setShowToast(!showToast); if (item) { - unequipItem.mutate({ item }); + unequipItem.mutate({ item, callback: {} }); } }; diff --git a/frontend/src/pages/sell/character-sell.tsx b/frontend/src/pages/sell/character-sell.tsx index e6c81b31a..333045a5a 100644 --- a/frontend/src/pages/sell/character-sell.tsx +++ b/frontend/src/pages/sell/character-sell.tsx @@ -5,6 +5,8 @@ import { ErrorView } from "../../components"; import { useMyCharacter, useSellCharacter } from "../../service"; import { Sell } from "./sell"; import { SellData } from "./types"; +import { MakeOfferCallback } from "../../interfaces"; +import { useUserStateDispatch } from "../../context/user"; export const CharacterSell = () => { const { id } = useParams<"id">(); @@ -13,13 +15,20 @@ export const CharacterSell = () => { const sellCharacter = useSellCharacter(Number(idString)); const [character] = useMyCharacter(Number(idString)); const [characterCopy] = useState(character); + const userDispatch = useUserStateDispatch(); const [isPlacedInShop, setIsPlacedInShop] = useState(false); const [data, setData] = useState({ price: 0 }); + const handleResult: MakeOfferCallback = { + seated: () => { + setIsPlacedInShop(true); + userDispatch({ type: "SET_SELECTED", payload: "" }); + }, + }; + const sendOfferHandler = async (data: SellData) => { - if (data.price < 1) return; // We don't want to sell for free in case someone managed to fool the frontend - await sellCharacter.callback(data.price, () => setIsPlacedInShop(true)); + await sellCharacter.sendOffer(data.price, handleResult); }; const characterName = useMemo(() => character?.nft.name, [character]); diff --git a/frontend/src/pages/sell/item-sell.tsx b/frontend/src/pages/sell/item-sell.tsx index edffaee54..025cbf77c 100644 --- a/frontend/src/pages/sell/item-sell.tsx +++ b/frontend/src/pages/sell/item-sell.tsx @@ -5,7 +5,7 @@ import { ErrorView } from "../../components"; import { useSellItem } from "../../service"; import { Sell } from "./sell"; import { SellData } from "./types"; -import { Category, isItemCategory } from "../../interfaces"; +import { Category, MakeOfferCallback, isItemCategory } from "../../interfaces"; export const ItemSell = () => { const { name, category } = useParams<"category" | "name">(); @@ -13,9 +13,13 @@ export const ItemSell = () => { const sellItem = useSellItem(name, category as Category); const [data, setData] = useState({ price: 0 }); + const handleResult: MakeOfferCallback = { + settled: () => setIsPlacedInShop(true), + }; + const sendOfferHandler = async (data: SellData) => { if (data.price < 1) return; // We don't want to sell for free in case someone managed to fool the frontend - await sellItem.callback(data.price, () => setIsPlacedInShop(true) ); + await sellItem.sendOffer(data.price, handleResult); }; if (!data || !isItemCategory(category)) return ; diff --git a/frontend/src/service/character.ts b/frontend/src/service/character.ts index 21265c973..4f773c50b 100644 --- a/frontend/src/service/character.ts +++ b/frontend/src/service/character.ts @@ -1,6 +1,6 @@ import { useMutation } from "react-query"; -import { CharacterCreation, CharacterInMarket, ExtendedCharacter, MarketMetrics } from "../interfaces"; +import { MakeOfferCallback, CharacterInMarket, ExtendedCharacter, MarketMetrics } from "../interfaces"; import { useCallback, useEffect, useMemo, useState } from "react"; import { extendCharacters } from "./transform-character"; import { useAgoricContext, useAgoricState } from "../context/agoric"; @@ -62,15 +62,11 @@ export const useSelectedCharacter = (): [ExtendedCharacter | undefined, boolean] }; export const useMyCharactersForSale = () => { - const [ - { - chainStorageWatcher, - }, - ] = useAgoricContext(); + const [{ chainStorageWatcher }] = useAgoricContext(); const wallet = useWalletState(); // stringified ExtendedCharacterBackend[], for some reason the state goes wild if I make it an array - const [offerCharacters, setOfferCharacters] = useState("[]"); + const [offerCharacters, setOfferCharacters] = useState("[]"); // adding items to characters from offers useEffect(() => { @@ -157,7 +153,7 @@ export const useCreateCharacter = () => { const service = useAgoricState(); const instance = service.contracts.kread.instance; const istBrand = service.tokenInfo.ist.brand; - return useMutation(async (body: CharacterCreation): Promise => { + return useMutation(async (body: { name: string; callback: MakeOfferCallback }): Promise => { if (!body.name) throw new Error("Name not specified"); await mintCharacter({ name: body.name, @@ -166,10 +162,7 @@ export const useCreateCharacter = () => { istBrand: istBrand, makeOffer: service.walletConnection.makeOffer, }, - callback: async () => { - console.info("MintCharacter call settled"); - }, - errorCallback: body.setError, + callback: body.callback, }); }); }; @@ -179,19 +172,17 @@ export const useSellCharacter = (characterId: number) => { const [service] = useAgoricContext(); const wallet = useWalletState(); const [characters] = useMyCharacters(); - const userDispatch = useUserStateDispatch(); const [isLoading, setIsLoading] = useState(false); const instance = service.contracts.kread.instance; const charBrand = service.tokenInfo.character.brand; - const callback = useCallback( - async (price: number, successCallback: () => void) => { + const sendOffer = useCallback( + async (price: number, callback: MakeOfferCallback) => { const found = characters.find((character) => character.nft.id === characterId); if (!found) return; const characterToSell = { ...found.nft, id: Number(found.nft.id) }; const uISTPrice = ISTTouIST(price); - - setIsLoading(true); + await marketService.sellCharacter({ character: characterToSell, price: BigInt(uISTPrice), @@ -201,18 +192,16 @@ export const useSellCharacter = (characterId: number) => { makeOffer: service.walletConnection.makeOffer, istBrand: service.tokenInfo.ist.brand, }, - callback: async () => { - console.info("SellCharacter call settled"); - setIsLoading(false); - userDispatch({ type: "SET_SELECTED", payload: "" }); - }, + callback: { + ...callback, + setIsLoading: setIsLoading + } }); - successCallback(); }, - [characterId, characters, wallet, service, userDispatch], + [characterId, characters, wallet, service], ); - return { callback, isLoading }; + return { sendOffer, isLoading }; }; export const useBuyCharacter = (characterId: string) => { @@ -228,32 +217,35 @@ export const useBuyCharacter = (characterId: string) => { setIsLoading(false); }, [characterId, service.offers]); - const callback = useCallback(async () => { - const found = characters.find((character) => character.id === characterId); - if (!found) return; - const characterToBuy = { - ...found, - character: found.character, - }; + const sendOffer = useCallback( + async (callback: MakeOfferCallback) => { + const found = characters.find((character) => character.id === characterId); + if (!found) return; + const characterToBuy = { + ...found, + character: found.character, + }; - setIsLoading(true); - await marketService.buyCharacter({ - character: characterToBuy.character, - price: BigInt(characterToBuy.sell.price + characterToBuy.sell.platformFee + characterToBuy.sell.royalty), - service: { - kreadInstance: instance, - characterBrand: charBrand, - makeOffer: service.walletConnection.makeOffer, - istBrand, - }, - callback: async () => { - console.info("BuyCharacter call settled"); - setIsLoading(false); - }, - }); - }, [characterId, characters, wallet, service]); + setIsLoading(true); + await marketService.buyCharacter({ + character: characterToBuy.character, + price: BigInt(characterToBuy.sell.price + characterToBuy.sell.platformFee + characterToBuy.sell.royalty), + service: { + kreadInstance: instance, + characterBrand: charBrand, + makeOffer: service.walletConnection.makeOffer, + istBrand, + }, + callback: { + ...callback, + setIsLoading: setIsLoading, + }, + }); + }, + [characterId, characters, wallet, service], + ); - return { callback, isLoading }; + return { sendOffer, isLoading }; }; export const useGetCharacterInShopById = (id: string): [CharacterInMarket | undefined, boolean] => { diff --git a/frontend/src/service/character/inventory.ts b/frontend/src/service/character/inventory.ts index f16f7354a..9572763b3 100644 --- a/frontend/src/service/character/inventory.ts +++ b/frontend/src/service/character/inventory.ts @@ -1,6 +1,7 @@ import { makeCopyBag } from "@agoric/store"; -import { Character, Item } from "../../interfaces"; +import { Character, Item, MakeOfferCallback } from "../../interfaces"; import { urlToCid } from "../../util/other"; +import { formOfferResultCallback } from "../../util/contract-callbacks"; // TODO: Use makeOffer status callback for errors interface UnequipItem { @@ -13,7 +14,7 @@ interface UnequipItem { itemBrand: any; makeOffer: any; }; - callback: () => Promise; + callback: MakeOfferCallback; } const unequipItem = async ({ item, character, service, callback }: UnequipItem): Promise => { @@ -55,19 +56,8 @@ const unequipItem = async ({ item, character, service, callback }: UnequipItem): give, }; - - service.makeOffer(spec, proposal, undefined, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, undefined, formOfferResultCallback(callback)); }; interface UnequipAllItems { @@ -77,7 +67,7 @@ interface UnequipAllItems { characterBrand: any; makeOffer: any; }; - callback: () => Promise; + callback: MakeOfferCallback; } const unequipAll = async ({ character, service, callback }: UnequipAllItems): Promise => { @@ -107,18 +97,8 @@ const unequipAll = async ({ character, service, callback }: UnequipAllItems): Pr give, }; - service.makeOffer(spec, proposal, undefined, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, undefined, formOfferResultCallback(callback)); }; interface EquipItem { @@ -130,7 +110,7 @@ interface EquipItem { itemBrand: any; makeOffer: any; }; - callback: () => Promise; + callback: MakeOfferCallback; } const equipItem = async ({ item, character, service, callback }: EquipItem): Promise => { @@ -145,7 +125,7 @@ const equipItem = async ({ item, character, service, callback }: EquipItem): Pro ...item, image: urlToCid(item.image), thumbnail: urlToCid(item.thumbnail), -}; + }; const spec = { source: "contract", @@ -170,18 +150,8 @@ const equipItem = async ({ item, character, service, callback }: EquipItem): Pro give, }; - service.makeOffer(spec, proposal, undefined, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, undefined, formOfferResultCallback(callback)); }; interface SwapItems { @@ -194,7 +164,7 @@ interface SwapItems { itemBrand: any; makeOffer: any; }; - callback: () => Promise; + callback: MakeOfferCallback; } const swapItems = async ({ giveItem, wantItem, character, service, callback }: SwapItems): Promise => { @@ -209,13 +179,12 @@ const swapItems = async ({ giveItem, wantItem, character, service, callback }: S ...giveItem, image: urlToCid(giveItem.image), thumbnail: urlToCid(giveItem.thumbnail), -}; + }; const itemWant: Item = { ...wantItem, image: urlToCid(wantItem.image), thumbnail: urlToCid(wantItem.thumbnail), -}; - + }; const spec = { source: "contract", @@ -241,18 +210,8 @@ const swapItems = async ({ giveItem, wantItem, character, service, callback }: S give, }; - service.makeOffer(spec, proposal, undefined, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, undefined, formOfferResultCallback(callback)); }; export const inventoryService = { unequipItem, equipItem, unequipAll, swapItems }; diff --git a/frontend/src/service/character/market.ts b/frontend/src/service/character/market.ts index 11bd267c1..4a2dc5dc5 100644 --- a/frontend/src/service/character/market.ts +++ b/frontend/src/service/character/market.ts @@ -1,7 +1,7 @@ import { makeCopyBag } from "@agoric/store"; -import { Character, Item } from "../../interfaces"; +import { Character, Item, MakeOfferCallback } from "../../interfaces"; import { urlToCid } from "../../util/other"; -// TODO: Use makeOffer status callback for errors +import { formOfferResultCallback } from "../../util/contract-callbacks"; interface CharacterMarketAction { character: Character; @@ -12,7 +12,7 @@ interface CharacterMarketAction { istBrand: any; makeOffer: any; }; - callback: () => Promise; + callback: MakeOfferCallback; } const sellCharacter = async ({ character, price, service, callback }: CharacterMarketAction): Promise => { @@ -40,19 +40,8 @@ const sellCharacter = async ({ character, price, service, callback }: CharacterM give, }; - service.makeOffer(spec, proposal, undefined, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - callback(); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, undefined, formOfferResultCallback(callback)); }; const buyCharacter = async ({ character, price, service, callback }: CharacterMarketAction): Promise => { @@ -81,19 +70,8 @@ const buyCharacter = async ({ character, price, service, callback }: CharacterMa give, }; - service.makeOffer(spec, proposal, undefined, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - callback(); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, undefined, formOfferResultCallback(callback)); }; interface ItemMarketAction { @@ -106,7 +84,7 @@ interface ItemMarketAction { istBrand: any; makeOffer: any; }; - callback: () => Promise; + callback: MakeOfferCallback; } const sellItem = async ({ item, price, service, callback }: ItemMarketAction): Promise => { @@ -117,7 +95,7 @@ const sellItem = async ({ item, price, service, callback }: ItemMarketAction): P ...item, image: urlToCid(item.image), thumbnail: urlToCid(item.thumbnail), -}; + }; const spec = { id: "custom-id", @@ -138,20 +116,8 @@ const sellItem = async ({ item, price, service, callback }: ItemMarketAction): P want, give, }; - - service.makeOffer(spec, proposal, undefined, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - callback(); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, undefined, formOfferResultCallback(callback)); }; interface SellItemBatchAction { @@ -235,20 +201,8 @@ const buyItem = async ({ entryId, item, price, service, callback }: ItemMarketAc want, give, }; - - service.makeOffer(spec, proposal, { entryId: Number(entryId) }, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - } - if (status === "refunded") { - console.error("Offer refunded", data); - callback(); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if( callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, { entryId: Number(entryId) }, formOfferResultCallback(callback)); }; export const marketService = { sellCharacter, buyCharacter, sellItem, sellItemBatch, buyItem }; diff --git a/frontend/src/service/character/mint.ts b/frontend/src/service/character/mint.ts index 2ae29b514..2f3d64e83 100644 --- a/frontend/src/service/character/mint.ts +++ b/frontend/src/service/character/mint.ts @@ -1,4 +1,6 @@ import { MINTING_COST } from "../../constants"; +import { MakeOfferCallback } from "../../interfaces"; +import { formOfferResultCallback } from "../../util/contract-callbacks"; // TODO: Use makeOffer status callback for errors @@ -9,11 +11,10 @@ interface MintCharacter { istBrand: any; makeOffer: any; }; - callback: () => Promise; - errorCallback?: (error: string) => void; + callback: MakeOfferCallback; } -export const mintCharacter = async ({ name, service, callback, errorCallback }: MintCharacter): Promise => { +export const mintCharacter = async ({ name, service, callback }: MintCharacter): Promise => { const instance = service.kreadInstance; const spec = { source: "contract", @@ -31,17 +32,6 @@ export const mintCharacter = async ({ name, service, callback, errorCallback }: give, }; - service.makeOffer(spec, proposal, offerArgs, ({ status, data }: { status: string; data: object }) => { - if (status === "error") { - console.error("Offer error", data); - if(errorCallback) errorCallback(JSON.stringify(data)); - } - if (status === "refunded") { - console.error("Offer refunded", data); - } - if (status === "accepted") { - console.info("Offer accepted", data); - callback(); - } - }); + if (callback.setIsLoading) callback.setIsLoading(true); + service.makeOffer(spec, proposal, offerArgs, formOfferResultCallback(callback)); }; diff --git a/frontend/src/service/items.ts b/frontend/src/service/items.ts index 8b1050021..d8ca63f9c 100644 --- a/frontend/src/service/items.ts +++ b/frontend/src/service/items.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from "react"; import { useMutation } from "react-query"; -import { Category, Item, ItemInMarket, MarketMetrics, Rarity } from "../interfaces"; +import { Category, Item, ItemInMarket, MakeOfferCallback, MarketMetrics, Rarity } from "../interfaces"; import { ISTTouIST, mediate, useFilterItems, useFilterItemsInShop } from "../util"; import { useAgoricContext } from "../context/agoric"; import { useOffers } from "./offers"; @@ -142,8 +142,8 @@ export const useSellItem = (itemName: string | undefined, itemCategory: Category const { items } = useUserState(); const [isLoading, setIsLoading] = useState(false); - const callback = useCallback( - async (price: number, setPlacedInShop: () => void) => { + const sendOffer = useCallback( + async (price: number, callback: MakeOfferCallback) => { try { const found = items.find((item) => item.name === itemName && item.category === itemCategory); if (!found) return; @@ -152,8 +152,6 @@ export const useSellItem = (itemName: string | undefined, itemCategory: Category const itemBrand = service.tokenInfo.item.brand; const uISTPrice = ISTTouIST(price); - setIsLoading(true); - await marketService.sellItem({ item: itemToSell, price: BigInt(uISTPrice), @@ -163,12 +161,11 @@ export const useSellItem = (itemName: string | undefined, itemCategory: Category makeOffer: service.walletConnection.makeOffer, istBrand: service.tokenInfo.ist.brand, }, - callback: async () => { - console.info("SellItem call settled"); - setIsLoading(false); - }, + callback: { + ...callback, + setIsLoading: setIsLoading + } }); - setPlacedInShop(); return true; } catch (error) { console.warn(error); @@ -178,7 +175,7 @@ export const useSellItem = (itemName: string | undefined, itemCategory: Category [itemName, itemCategory, items, service], ); - return { callback, isLoading }; + return { sendOffer, isLoading }; }; export const useBuyItem = (itemToBuy: ItemInMarket | undefined) => { @@ -193,15 +190,13 @@ export const useBuyItem = (itemToBuy: ItemInMarket | undefined) => { const itemBrand = service.tokenInfo.item.brand; const istBrand = service.tokenInfo.ist.brand; - const callback = useCallback( - async (setIsAwaitingApprovalToFalse: () => void) => { + const sendOffer = useCallback( + async (callback: MakeOfferCallback) => { try { if (!itemToBuy) return; const { forSale, equippedTo, activity, ...itemObject } = itemToBuy.item; itemToBuy.item = itemObject; - setIsLoading(true); - return await marketService.buyItem({ entryId: itemToBuy.id, item: itemToBuy.item, @@ -212,25 +207,23 @@ export const useBuyItem = (itemToBuy: ItemInMarket | undefined) => { makeOffer: service.walletConnection.makeOffer, istBrand, }, - callback: async () => { - console.info("BuyItem call settled"); - setIsLoading(false); - setIsAwaitingApprovalToFalse(); - }, + callback: { + ...callback, + setIsLoading: setIsLoading + } }); } catch (error) { console.warn(error); setIsError(true); - setIsAwaitingApprovalToFalse(); } }, [itemToBuy, items, wallet, service], ); - return { callback, isLoading, isError }; + return { sendOffer, isLoading, isError }; }; -export const useEquipItem = (callback?: React.Dispatch>) => { +export const useEquipItem = () => { const [service] = useAgoricContext(); const { selected: character } = useUserState(); const { character: charactersInWallet } = useWalletState(); @@ -239,7 +232,7 @@ export const useEquipItem = (callback?: React.Dispatch => { + return useMutation(async (body: { item: Item; currentlyEquipped?: Item; callback: MakeOfferCallback }): Promise => { if (!character || !body.item) { console.error("Could not find item or character"); return; @@ -252,6 +245,7 @@ export const useEquipItem = (callback?: React.Dispatch { - console.info("Swap call settled"); - - if (callback) callback(body.item); - - // Using a delay to prevent the character from disappearing when making inventory calls - setTimeout(() => userStateDispatch({ type: "END_INVENTORY_CALL" }), INVENTORY_CALL_FETCH_DELAY); - }, + callback: { + ...body.callback, + settled: () => { + setTimeout(() => userStateDispatch({ type: "END_INVENTORY_CALL" }), INVENTORY_CALL_FETCH_DELAY); + } + } }); } else { await inventoryService.equipItem({ @@ -281,20 +273,19 @@ export const useEquipItem = (callback?: React.Dispatch { - console.info("Equip call settled"); - - if (callback) callback(body.item); - - // Using a delay to prevent the character from disappearing when making inventory calls - setTimeout(() => userStateDispatch({ type: "END_INVENTORY_CALL" }), INVENTORY_CALL_FETCH_DELAY); + callback: { + ...body.callback, + settled: () => { + if (body.callback.settled) body.callback.settled(); + setTimeout(() => userStateDispatch({ type: "END_INVENTORY_CALL" }), INVENTORY_CALL_FETCH_DELAY); + } }, }); } }); }; -export const useUnequipItem = (callback?: () => void) => { +export const useUnequipItem = () => { const [service] = useAgoricContext(); const { characters: ownedCharacters } = useUserState(); const userStateDispatch = useUserStateDispatch(); @@ -302,7 +293,7 @@ export const useUnequipItem = (callback?: () => void) => { const charBrand = service.tokenInfo.character.brand; const itemBrand = service.tokenInfo.item.brand; - return useMutation(async (body: { item: Item }) => { + return useMutation(async (body: { item: Item; callback: MakeOfferCallback }) => { if (!body.item) return; userStateDispatch({ type: "START_INVENTORY_CALL" }); @@ -322,13 +313,13 @@ export const useUnequipItem = (callback?: () => void) => { itemBrand, makeOffer: service.walletConnection.makeOffer, }, - callback: async () => { - console.info("Unequip call settled"); - if (callback) callback(); - - // Using a delay to prevent the character from disappearing when making inventory calls - setTimeout(() => userStateDispatch({ type: "END_INVENTORY_CALL" }), INVENTORY_CALL_FETCH_DELAY); - }, + callback: { + ...body.callback, + settled: () => { + if (body.callback.settled) body.callback.settled(); + setTimeout(() => userStateDispatch({ type: "END_INVENTORY_CALL" }), INVENTORY_CALL_FETCH_DELAY); + } + } }); }); }; diff --git a/frontend/src/util/contract-callbacks.ts b/frontend/src/util/contract-callbacks.ts new file mode 100644 index 000000000..37fed0fc2 --- /dev/null +++ b/frontend/src/util/contract-callbacks.ts @@ -0,0 +1,30 @@ +import { MakeOfferCallback, OFFER_STATUS, OfferStatusType } from "../interfaces"; + +export const formOfferResultCallback = + (callback: MakeOfferCallback) => + ({ status, data }: { status: OfferStatusType; data: object }) => { + switch (status) { + case OFFER_STATUS.error: { + console.error("Offer error", JSON.stringify(data)); + if (callback.error) callback.error(); + break; + } + case OFFER_STATUS.refunded: { + console.error("Offer refunded", JSON.stringify(data)); + if (callback.refunded) callback.refunded(); + break; + } + case OFFER_STATUS.accepted: { + console.info("Offer accepted", JSON.stringify(data)); + if (callback.accepted) callback.accepted(); + break; + } + case OFFER_STATUS.seated: { + console.log("Offer seated"); + if(callback.seated) callback.seated(); + break; + } + } + if (callback.setIsLoading) callback.setIsLoading(false); + if(callback.settled) callback.settled(); + }; \ No newline at end of file