diff --git a/.env.development b/.env.development index 307861c..c6f2f20 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,6 @@ NEXT_PUBLIC_CHAIN_ENV=mainnet -NEXT_PUBLIC_APP_URL=https://flowview.vercel.app/ +NEXT_PUBLIC_APP_URL=http://localhost:3000/ NEXT_PUBLIC_ACCESS_NODE_API=https://floral-special-valley.flow-mainnet.quiknode.pro NEXT_PUBLIC_WALLET_DISCOVERY=https://fcl-discovery.onflow.org/authn @@ -23,3 +23,5 @@ NEXT_PUBLIC_FUNGIBLE_TOKEN_SWITCHBOARD_ADDRESS=0xf233dcee88fe0abe NEXT_PUBLIC_FLOWBOX_ADDRESS=0x1b3930856571a52b NEXT_PUBLIC_ACCOUNTBOOKMARK_ADDRESS=0x39b144ab4d348e2b NEXT_PUBLIC_NFTSTOREFRONTV2_ADDRESS=0x4eb8a10cb9f87357 + +NEXT_PUBLIC_HYBRIDCUSTODY_ADDRESS=0xd8a7e05a7ac670c0 \ No newline at end of file diff --git a/.env.production b/.env.production index 307861c..13fe6ff 100644 --- a/.env.production +++ b/.env.production @@ -1,6 +1,6 @@ NEXT_PUBLIC_CHAIN_ENV=mainnet -NEXT_PUBLIC_APP_URL=https://flowview.vercel.app/ +NEXT_PUBLIC_APP_URL=https://flowview.app/ NEXT_PUBLIC_ACCESS_NODE_API=https://floral-special-valley.flow-mainnet.quiknode.pro NEXT_PUBLIC_WALLET_DISCOVERY=https://fcl-discovery.onflow.org/authn @@ -23,3 +23,4 @@ NEXT_PUBLIC_FUNGIBLE_TOKEN_SWITCHBOARD_ADDRESS=0xf233dcee88fe0abe NEXT_PUBLIC_FLOWBOX_ADDRESS=0x1b3930856571a52b NEXT_PUBLIC_ACCOUNTBOOKMARK_ADDRESS=0x39b144ab4d348e2b NEXT_PUBLIC_NFTSTOREFRONTV2_ADDRESS=0x4eb8a10cb9f87357 +NEXT_PUBLIC_HYBRIDCUSTODY_ADDRESS=0xd8a7e05a7ac670c0 diff --git a/components/common/Siderbar.js b/components/common/Siderbar.js index 810d246..adeb169 100644 --- a/components/common/Siderbar.js +++ b/components/common/Siderbar.js @@ -16,10 +16,16 @@ export default function Sidebar(props) { { id: "5", label: `Storefront`, link: { pathname: "/account/[account]/storefront", query: { account: account } } }, { id: "6", label: `Contract`, link: { pathname: "/account/[account]/contract", query: { account: account } } }, { - id: "7", label: "Storage", subItems: [ - { id: "7-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } }, - { id: "7-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } }, - { id: "7-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } }, + id: "7", label: `Acct Linking`, subItems: [ + { id: "7-0", isSubItem: true, label: "Owned Acct", smLabel: "Owned Acct", link: { pathname: "/account/[account]/hc/owned_account", query: { account: account } } }, + { id: "7-1", isSubItem: true, label: "HC Manager", smLabel: "HC Manager", link: { pathname: "/account/[account]/hc/manager", query: { account: account } } }, + ] + }, + { + id: "8", label: "Storage", subItems: [ + { id: "8-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } }, + { id: "8-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } }, + { id: "8-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } }, ] } // { id: "5", label: `Analyzer`, link: { pathname: "/account/[account]/analyzer", query: { account: account } } }, @@ -33,10 +39,16 @@ export default function Sidebar(props) { { id: "4", label: `Collection`, link: { pathname: "/account/[account]/collection", query: { account: account } } }, { id: "5", label: `Contract`, link: { pathname: "/account/[account]/contract", query: { account: account } } }, { - id: "6", label: "Storage", subItems: [ - { id: "6-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } }, - { id: "6-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } }, - { id: "6-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } }, + id: "6", label: `Acct Linking`, subItems: [ + { id: "6-0", isSubItem: true, label: "Owned Acct", smLabel: "Owned Acct", link: { pathname: "/account/[account]/hc/owned_account", query: { account: account } } }, + { id: "6-1", isSubItem: true, label: "HC Manager", smLabel: "HC Manager", link: { pathname: "/account/[account]/hc/manager", query: { account: account } } }, + ] + }, + { + id: "7", label: "Storage", subItems: [ + { id: "7-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } }, + { id: "7-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } }, + { id: "7-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } }, ] } // { id: "5", label: `Analyzer`, link: { pathname: "/account/[account]/analyzer", query: { account: account } } }, @@ -78,7 +90,7 @@ export default function Sidebar(props) {
-
+
{menuItems.map(({ label: label, ...menu }, index) => { const classes = getNavItemClasses(menu) return ( diff --git a/components/hybrid_custody/ChildView.js b/components/hybrid_custody/ChildView.js new file mode 100644 index 0000000..e791863 --- /dev/null +++ b/components/hybrid_custody/ChildView.js @@ -0,0 +1,71 @@ +import { useRouter } from "next/router"; +import publicConfig from "../../publicConfig"; +import { useRecoilState } from "recoil"; +import { showSetupDisplayState, transactionInProgressState, transactionStatusState } from "../../lib/atoms"; +import { removeChildAccount, removeChildFromChild, setupChildAccountDisplay } from "../../flow/hc_transactions"; +import { useSWRConfig } from "swr"; +import OwnedDisplayView from "./OwnedDisplayView"; + +export default function ChildView(props) { + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + const [showSetupDisplay, setShowSetupDisplay] = useRecoilState(showSetupDisplayState) + const { mutate } = useSWRConfig() + + const router = useRouter() + const { child, account, user } = props + + return ( +
+
+
+ +
+ {/* */} + + +
+
+ { + child.display ? + : null + } +
+
+ ) +} \ No newline at end of file diff --git a/components/hybrid_custody/OwnedDisplayView.js b/components/hybrid_custody/OwnedDisplayView.js new file mode 100644 index 0000000..298ad9a --- /dev/null +++ b/components/hybrid_custody/OwnedDisplayView.js @@ -0,0 +1,45 @@ +import Image from "next/image" +import publicConfig from "../../publicConfig" +import { getImageSrcFromMetadataViewsFile } from "../../lib/utils" + +export default function OwnedDisplayView(props) { + const { display, style } = props + + if (style == "Small") { + return ( +
+
+
+ +
+
+ + +
+
+
+ ) + } + + return ( +
+
+
+ +
+
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/components/hybrid_custody/OwnedView.js b/components/hybrid_custody/OwnedView.js new file mode 100644 index 0000000..045b9dc --- /dev/null +++ b/components/hybrid_custody/OwnedView.js @@ -0,0 +1,59 @@ +import { useRouter } from "next/router"; +import publicConfig from "../../publicConfig"; +import { useRecoilState } from "recoil"; +import { showSetupDisplayState, showTransferOwnershipState, transactionInProgressState, transactionStatusState } from "../../lib/atoms"; +import { removeChildAccount, removeChildFromChild, setupChildAccountDisplay } from "../../flow/hc_transactions"; +import { useSWRConfig } from "swr"; +import OwnedDisplayView from "./OwnedDisplayView"; + +export default function OwnedView(props) { + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + const [showSetupDisplay, setShowSetupDisplay] = useRecoilState(showSetupDisplayState) + const [showTransferOwnership, setShowTransferOwnership] = useRecoilState(showTransferOwnershipState) + const { mutate } = useSWRConfig() + + const router = useRouter() + const { child, account, user } = props + + return ( +
+
+
+ +
+ + + {/* */} +
+
+ { + child.display ? + : null + } +
+
+ ) +} \ No newline at end of file diff --git a/components/hybrid_custody/ParentView.js b/components/hybrid_custody/ParentView.js new file mode 100644 index 0000000..5b83419 --- /dev/null +++ b/components/hybrid_custody/ParentView.js @@ -0,0 +1,78 @@ +import { useRouter } from "next/router"; +import publicConfig from "../../publicConfig"; +import { useRecoilState } from "recoil"; +import { transactionInProgressState, transactionStatusState } from "../../lib/atoms"; +import { removeParentFromChild } from "../../flow/hc_transactions"; +import { useSWRConfig } from "swr"; + +export default function ParentView(props) { + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + const { mutate } = useSWRConfig() + + const router = useRouter() + const { parent, account, user } = props + + return ( +
+
+ { + !parent.isClaimed ? +
+ +
+ :
+ +
+ } +
+ +
+ +
+
+ + +
+ + +
+ + ) +} \ No newline at end of file diff --git a/components/hybrid_custody/PublishToParentModal.js b/components/hybrid_custody/PublishToParentModal.js new file mode 100644 index 0000000..277d27a --- /dev/null +++ b/components/hybrid_custody/PublishToParentModal.js @@ -0,0 +1,206 @@ +import { Fragment, useEffect, useRef, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { useRecoilState } from "recoil" +import { showPublishToParentState, transactionStatusState, transactionInProgressState } from '../../lib/atoms' +import * as fcl from "@onflow/fcl"; +import { isValidFlowAddress, isValidPositiveFlowDecimals, isValidPositiveNumber, isValidUrl } from '../../lib/utils' +import { useRouter } from 'next/router' +import { useSWRConfig } from 'swr' +import { publishToParent, setupOwnedAccount, setupOwnedAccountAndPublishToParent } from '../../flow/hc_transactions'; + +export default function PublishToParentModal(props) { + const router = useRouter() + const { mutate } = useSWRConfig() + const {account} = props + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + + const [showPublishToParent, setShowPublishToParent] = useRecoilState(showPublishToParentState) + + const [parent, setParent] = useState("") + const [parentError, setParentError] = useState(null) + const [factory, setFactory] = useState("") + const [factoryError, setFactoryError] = useState(null) + const [filter, setFilter] = useState("") + const [filterError, setFilterError] = useState(null) + + useEffect(() => fcl.currentUser.subscribe(setUser), []) + const [user, setUser] = useState({ loggedIn: null }) + + const cancelButtonRef = useRef(null) + + return ( + + + +
+ + +
+
+ + +
+
+ + {"Publish To Parent"} + +
+
+ +
+ { + setParentError(null) + setParent("") + if (e.target.value === "") { + return + } + + if (!isValidFlowAddress(e.target.value)) { + setParentError("Invalid address") + return + } + setParent(e.target.value) + }} + /> +
+ { + parentError ? + : null + } +
+
+ +
+ { + setFactoryError(null) + setFactory("") + if (e.target.value === "") { + return + } + + if (!isValidFlowAddress(e.target.value)) { + setFactoryError("Invalid address") + return + } + setFactory(e.target.value) + }} + /> +
+ { + factoryError ? + : null + } +
+
+ +
+ { + setFilterError(null) + setFilter("") + if (e.target.value === "") { + return + } + + if (!isValidFlowAddress(e.target.value)) { + setFilterError("Invalid address") + return + } + setFilter(e.target.value) + }} + /> +
+ { + filterError ? + : null + } +
+
+
+
+
+ + +
+
+
+
+
+
+
+ ) +} diff --git a/components/hybrid_custody/RedeemAccountModal.js b/components/hybrid_custody/RedeemAccountModal.js new file mode 100644 index 0000000..28f4c88 --- /dev/null +++ b/components/hybrid_custody/RedeemAccountModal.js @@ -0,0 +1,140 @@ +import { Fragment, useEffect, useRef, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { useRecoilState } from "recoil" +import { showRedeemAccountState, transactionStatusState, transactionInProgressState } from '../../lib/atoms' +import * as fcl from "@onflow/fcl"; +import { isValidFlowAddress, isValidPositiveFlowDecimals, isValidPositiveNumber, isValidUrl } from '../../lib/utils' +import { useRouter } from 'next/router' +import { useSWRConfig } from 'swr' +import { acceptOwnership, redeemAccount } from '../../flow/hc_transactions'; + +export default function RedeemAccountModal(props) { + const router = useRouter() + const { mutate } = useSWRConfig() + const {account} = props + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + + const [showRedeemAccount, setShowRedeemAccount] = useRecoilState(showRedeemAccountState) + + const [childAddress, setChildAddress] = useState("") + const [childAddressError, setChildAddressError] = useState(null) + + useEffect(() => fcl.currentUser.subscribe(setUser), []) + const [user, setUser] = useState({ loggedIn: null }) + + const cancelButtonRef = useRef(null) + + return ( + + setShowRedeemAccount(prev => ({ + ...prev, show: false + }))}> + +
+ + +
+
+ + +
+
+ + {showRedeemAccount.mode == "RedeemAccount" ? + "Redeem ChildAccount" : "Accept Ownership"} + +
+
+ +
+ { + setChildAddressError(null) + setChildAddress("") + if (e.target.value === "") { + return + } + + if (!isValidFlowAddress(e.target.value)) { + setChildAddressError("Invalid address") + return + } + setChildAddress(e.target.value) + }} + /> +
+ { + childAddressError ? + : null + } +
+
+
+
+
+ + +
+
+
+
+
+
+
+ ) +} diff --git a/components/hybrid_custody/SetupDisplayModal.js b/components/hybrid_custody/SetupDisplayModal.js new file mode 100644 index 0000000..03154aa --- /dev/null +++ b/components/hybrid_custody/SetupDisplayModal.js @@ -0,0 +1,190 @@ +import { Fragment, useEffect, useRef, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { useRecoilState } from "recoil" +import { showSetupDisplayState, transactionStatusState, transactionInProgressState } from '../../lib/atoms' +import * as fcl from "@onflow/fcl"; +import { isValidFlowAddress, isValidPositiveFlowDecimals, isValidPositiveNumber, isValidUrl } from '../../lib/utils' +import { useRouter } from 'next/router' +import { useSWRConfig } from 'swr' +import { setupChildAccountDisplay, setupOwnedAccount, setupOwnedAccountAndPublishToParent } from '../../flow/hc_transactions'; + +export default function SetupDisplayModal(props) { + const router = useRouter() + const { mutate } = useSWRConfig() + const { account } = props + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + + const [showSetupDisplay, setShowSetupDisplay] = useRecoilState(showSetupDisplayState) + const [name, setName] = useState("") + const [desc, setDesc] = useState("") + const thumbnailPlaceholder = "https://assets-global.website-files.com/5f734f4dbd95382f4fdfa0ea/6395e6749db8fe00a41cc279_flow-flow-logo.svg" + const [thumbnail, setThumbnail] = useState(thumbnailPlaceholder) + const [thumbnailError, setThumbnailError] = useState(null) + + useEffect(() => fcl.currentUser.subscribe(setUser), []) + const [user, setUser] = useState({ loggedIn: null }) + + const cancelButtonRef = useRef(null) + + return ( + + setShowSetupDisplay(prevState => ({ + ...prevState, show: false + }))}> + +
+ + +
+
+ + +
+
+ + { + showSetupDisplay.mode == "OwnedAccount" ? + "Setup OwnedAccount" : "Setup ChildAccount Display" + } + +
+
+ +
+ { + setName(e.target.value) + }} + /> +
+
+
+ +
+ { + setDesc(e.target.value) + }} + /> +
+
+
+ +
+ { + setThumbnailError(null) + if (e.target.value === "") { + return + } + + if (!isValidUrl(e.target.value)) { + setThumbnailError("Invalid URL") + return + } + setThumbnail(e.target.value) + }} + /> +
+ { + thumbnailError ? + : null + } +
+
+
+
+
+ + +
+
+
+
+
+
+
+ ) +} diff --git a/components/hybrid_custody/SetupHcManagerModal.js b/components/hybrid_custody/SetupHcManagerModal.js new file mode 100644 index 0000000..5f6a663 --- /dev/null +++ b/components/hybrid_custody/SetupHcManagerModal.js @@ -0,0 +1,175 @@ +import { Fragment, useEffect, useRef, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { useRecoilState } from "recoil" +import { showSetupHcManagerState, transactionStatusState, transactionInProgressState } from '../../lib/atoms' +import * as fcl from "@onflow/fcl"; +import { isValidFlowAddress, isValidPositiveFlowDecimals, isValidPositiveNumber, isValidUrl } from '../../lib/utils' +import { useRouter } from 'next/router' +import { useSWRConfig } from 'swr' +import { setupHcManager, setupOwnedAccount } from '../../flow/hc_transactions'; + +export default function SetupHcManagerModal(props) { + const router = useRouter() + const { mutate } = useSWRConfig() + const {account} = props + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + + const [showSetupHcManager, setShowSetupHcManager] = useRecoilState(showSetupHcManagerState) + + const [filter, setFilter] = useState("") + const [filterError, setFilterError] = useState(null) + const [filterPath, setFilterPath] = useState("") + const [filterPathError, setFilterPathError] = useState(null) + + useEffect(() => fcl.currentUser.subscribe(setUser), []) + const [user, setUser] = useState({ loggedIn: null }) + + const cancelButtonRef = useRef(null) + + return ( + + + +
+ + +
+
+ + +
+
+ + {"Setup HybridCustody Manager"} + +
+
+ +
+ { + setFilterError(null) + setFilter("") + if (e.target.value === "") { + return + } + + if (!isValidFlowAddress(e.target.value)) { + setFilterError("Invalid address") + return + } + setFilter(e.target.value) + }} + /> +
+ { + filterError ? + : null + } +
+
+ +
+
+ + /public/ + +
+ { + setFilterPathError(null) + setFilterPath("") + if (e.target.value === "") { + return + } + + setFilterPath(e.target.value) + }} + /> +
+ { + filterPathError ? + : null + } +
+
+
+
+
+ + +
+
+
+
+
+
+
+ ) +} diff --git a/components/hybrid_custody/TransferOwnerShipModal.js b/components/hybrid_custody/TransferOwnerShipModal.js new file mode 100644 index 0000000..d5f2a24 --- /dev/null +++ b/components/hybrid_custody/TransferOwnerShipModal.js @@ -0,0 +1,143 @@ +import { Fragment, useEffect, useRef, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { useRecoilState } from "recoil" +import { showTransferOwnershipState, transactionStatusState, transactionInProgressState } from '../../lib/atoms' +import * as fcl from "@onflow/fcl"; +import { isValidFlowAddress, isValidPositiveFlowDecimals, isValidPositiveNumber, isValidUrl } from '../../lib/utils' +import { useRouter } from 'next/router' +import { useSWRConfig } from 'swr' +import { transferOwnership, transferOwnershipFromManager } from '../../flow/hc_transactions'; + +export default function TransferOwnershipModal(props) { + const router = useRouter() + const { mutate } = useSWRConfig() + const {account} = props + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + + const [showTransferOwnership, setShowTransferOwnership] = useRecoilState(showTransferOwnershipState) + + const [ownerAddress, setOwnerAddress] = useState("") + const [ownerAddressError, setOwnerAddressError] = useState(null) + + useEffect(() => fcl.currentUser.subscribe(setUser), []) + const [user, setUser] = useState({ loggedIn: null }) + + const cancelButtonRef = useRef(null) + + return ( + + setShowTransferOwnership(prev => ({ + ...prev, show: false + }))}> + +
+ + +
+
+ + +
+
+ + {"Transfer Ownership"} + +
+
{`⚠️ You are transferring ownership of this account to another account. Please be certain.`}
+
+ +
+ { + setOwnerAddressError(null) + setOwnerAddress("") + if (e.target.value === "") { + return + } + + if (!isValidFlowAddress(e.target.value)) { + setOwnerAddressError("Invalid address") + return + } + setOwnerAddress(e.target.value) + }} + /> +
+ { + ownerAddressError ? + : null + } +
+
+
+
+
+ + +
+
+
+
+
+
+
+ ) +} diff --git a/flow/config.js b/flow/config.js index f3a0702..cad3805 100644 --- a/flow/config.js +++ b/flow/config.js @@ -19,4 +19,9 @@ config({ "0xFlowbox": publicConfig.flowboxAddress, "0xFlowviewAccountBookmark": publicConfig.accountBookmarkAddress, "0xNFTStorefrontV2": publicConfig.nftStorefrontV2Address, + + "0xHybridCustody": publicConfig.hybridCustodyAddress, + "0xCapabilityFactory": publicConfig.hybridCustodyAddress, + "0xCapabilityFilter": publicConfig.hybridCustodyAddress, + "0xCapabilityDelegator": publicConfig.hybridCustodyAddress }) \ No newline at end of file diff --git a/flow/hc_scripts.js b/flow/hc_scripts.js new file mode 100644 index 0000000..58668c1 --- /dev/null +++ b/flow/hc_scripts.js @@ -0,0 +1,27 @@ +import * as fcl from "@onflow/fcl" + +export const getOwnedAccountInfo = async (address) => { + const code = await (await fetch("/scripts/hc/get_owned_account_info.cdc")).text() + + const result = await fcl.query({ + cadence: code, + args: (arg, t) => [ + arg(address, t.Address) + ] + }) + + return result +} + +export const getHcManagerInfo = async (address) => { + const code = await (await fetch("/scripts/hc/get_hc_manager_info.cdc")).text() + + const result = await fcl.query({ + cadence: code, + args: (arg, t) => [ + arg(address, t.Address) + ] + }) + + return result +} diff --git a/flow/hc_transactions.js b/flow/hc_transactions.js new file mode 100644 index 0000000..0f18b4a --- /dev/null +++ b/flow/hc_transactions.js @@ -0,0 +1,323 @@ +import * as fcl from "@onflow/fcl" +import { txHandler } from "./transactions" + +export const setupOwnedAccountAndPublishToParent = async ( + parent, name, desc, thumbnail, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doSetupDisplayAndPublishToParent(parent, name, desc, thumbnail) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doSetupDisplayAndPublishToParent = async (parent, name, desc, thumbnail) => { + const code = await (await fetch("/transactions/hc/setup_owned_account_and_publish_to_parent.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(parent, t.Address), + arg(name, t.Optional(t.String)), + arg(desc, t.Optional(t.String)), + arg(thumbnail, t.Optional(t.String)) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const setupOwnedAccount = async ( + name, desc, thumbnail, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doSetupOwnedAccount(name, desc, thumbnail) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doSetupOwnedAccount = async (name, desc, thumbnail) => { + const code = await (await fetch("/transactions/hc/setup_owned_account.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(name, t.Optional(t.String)), + arg(desc, t.Optional(t.String)), + arg(thumbnail, t.Optional(t.String)) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const publishToParent = async ( + parent, factory, filter, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doPublishToParent(parent, factory, filter) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doPublishToParent = async (parent, factory, filter) => { + const code = await (await fetch("/transactions/hc/publish_to_parent.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(parent, t.Address), + arg(factory, t.Address), + arg(filter, t.Address) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const removeParentFromChild = async ( + parent, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doRemoveParentFromChild(parent) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doRemoveParentFromChild = async (parent) => { + const code = await (await fetch("/transactions/hc/remove_parent_from_child.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(parent, t.Address) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const setupHcManager = async ( + filter, filterPath, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doSetupHcManager(filter, filterPath) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doSetupHcManager = async (filter, filterPath) => { + const code = await (await fetch("/transactions/hc/setup_hc_manager.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(filter, t.Optional(t.Address)), + arg(filterPath, t.Optional(t.String)), + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const redeemAccount = async ( + childAddress, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doRedeemAccount(childAddress) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doRedeemAccount = async (childAddress) => { + const code = await (await fetch("/transactions/hc/redeem_account.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(childAddress, t.Address) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const acceptOwnership = async ( + childAddress, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doAcceptOwnership(childAddress) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doAcceptOwnership = async (childAddress) => { + const code = await (await fetch("/transactions/hc/accept_ownership.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(childAddress, t.Address) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const setupChildAccountDisplay = async ( + childAddress, name, desc, thumbnail, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doSetupChildAccountDisplay(childAddress, name, desc, thumbnail) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doSetupChildAccountDisplay = async (childAddress, name, desc, thumbnail) => { + const code = await (await fetch("/transactions/hc/setup_child_display.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(childAddress, t.Address), + arg(name, t.Optional(t.String)), + arg(desc, t.Optional(t.String)), + arg(thumbnail, t.Optional(t.String)) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const removeChildAccount = async ( + childAddress, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doRemoveChildAccount(childAddress) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doRemoveChildAccount = async (childAddress) => { + const code = await (await fetch("/transactions/hc/remove_child_account.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(childAddress, t.Address) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const transferOwnership = async ( + ownerAddress, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doTransferOwnership(ownerAddress) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doTransferOwnership = async (ownerAddress) => { + const code = await (await fetch("/transactions/hc/transfer_ownership.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(ownerAddress, t.Address) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} + +export const transferOwnershipFromManager = async ( + ownedAddress, + ownerAddress, + setTransactionInProgress, + setTransactionStatus +) => { + const txFunc = async () => { + return await doTransferOwnershipFromManager(ownedAddress, ownerAddress) + } + + return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus) +} + +const doTransferOwnershipFromManager = async (ownedAddress, ownerAddress) => { + const code = await (await fetch("/transactions/hc/transfer_ownership_from_manager.cdc")).text() + + const transactionId = fcl.mutate({ + cadence: code, + args: (arg, t) => [ + arg(ownedAddress, t.Address), + arg(ownerAddress, t.Address) + ], + proposer: fcl.currentUser, + payer: fcl.currentUser, + limit: 9999 + }) + + return transactionId +} \ No newline at end of file diff --git a/flow/scripts.js b/flow/scripts.js index f675c11..6ce9a78 100644 --- a/flow/scripts.js +++ b/flow/scripts.js @@ -212,12 +212,15 @@ export const bulkGetStoredItems = async (address) => { export const getStoredItems = async (address, paths) => { const code = await (await fetch("/scripts/storage/get_stored_items.cdc")).text() + const filteredPaths = paths.filter((item) => + item !== "BnGNFTCollection" && item !== "RacingTimeCollection" && item !== "FuseCollectiveCollection" && item !== "ARTIFACTV2Collection" + ) const items = await fcl.query({ cadence: code, args: (arg, t) => [ arg(address, t.Address), - arg(paths, t.Array(t.String)) + arg(filteredPaths, t.Array(t.String)) ] }) diff --git a/lib/atoms.js b/lib/atoms.js index 36ad5a9..55c7b9b 100644 --- a/lib/atoms.js +++ b/lib/atoms.js @@ -45,6 +45,31 @@ export const showCreateListingState = atom({ default: false }) +export const showSetupDisplayState = atom({ + key: "showSetupDisplayState", + default: {show: false, mode: "SetupOwnedAccount"} +}) + +export const showSetupHcManagerState = atom({ + key: "showSetupHcManagerState", + default: false +}) + +export const showRedeemAccountState = atom({ + key: "showRedeemAccountState", + default: {show: false, mode: "RedeemAccount"} +}) + +export const showTransferOwnershipState = atom({ + key: "showTransferOwnershipState", + default: {show: false, mode: "Simple"} +}) + +export const showPublishToParentState = atom({ + key: "showPublishToParentState", + default: false +}) + export const accountBookmarkState = atom({ key: "accountBookmarkState", default: {address: "Flow Address", note: "Note"} diff --git a/lib/utils.js b/lib/utils.js index cb4a289..5afdd9f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -44,6 +44,19 @@ export const isValidPositiveFlowDecimals = (number) => { } } +export const isValidUrl = (url) => { + // 创建一个正则表达式模式,用于匹配 URL + var pattern = new RegExp('^(https?:\\/\\/)?'+ // 协议 + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // 域名 + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // IP 地址 + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // 端口和路径 + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // 查询字符串 + '(\\#[-a-z\\d_]*)?$','i'); // 锚点 + + // 使用正则表达式进行匹配 + return pattern.test(url); +} + // TODO: replace this with fcl.account(address) export const isValidFlowAddress = (address) => { if (!address.startsWith("0x") || address.length != 18) { diff --git a/pages/account/[account]/hc/manager/index.js b/pages/account/[account]/hc/manager/index.js new file mode 100644 index 0000000..1e139da --- /dev/null +++ b/pages/account/[account]/hc/manager/index.js @@ -0,0 +1,190 @@ +import * as fcl from "@onflow/fcl" +import { useRouter } from "next/router" +import { useEffect, useState } from "react" +import useSWR from "swr" +import ItemsView from "../../../../../components/common/ItemsView" +import Layout from "../../../../../components/common/Layout" +import Spinner from "../../../../../components/common/Spinner" +import { isValidFlowAddress } from "../../../../../lib/utils" +import Custom404 from "../../404" +import { getHcManagerInfo, getOwnedAccountInfo } from "../../../../../flow/hc_scripts" +import { useRecoilState } from "recoil" +import { showRedeemAccountState, showSetupHcManagerState, transactionInProgressState, transactionStatusState } from "../../../../../lib/atoms" +import SetupDisplayModal from "../../../../../components/hybrid_custody/SetupDisplayModal" +import PublishToParentModal from "../../../../../components/hybrid_custody/PublishToParentModal" +import { setupOwnedAccount } from "../../../../../flow/hc_transactions" +import ParentView from "../../../../../components/hybrid_custody/ParentView" +import SetupHcManagerModal from "../../../../../components/hybrid_custody/SetupHcManagerModal" +import RedeemAccountModal from "../../../../../components/hybrid_custody/RedeemAccountModal" +import ChildView from "../../../../../components/hybrid_custody/ChildView" +import OwnedView from "../../../../../components/hybrid_custody/OwnedView" +import TransferOwnershipModal from "../../../../../components/hybrid_custody/TransferOwnerShipModal" + +const hcManagerInfoFetcher = async (funcName, address) => { + return getHcManagerInfo(address) +} + +export default function HybridCustodyManager(props) { + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + const [showSetupHcManager, setShowSetupHcManager] = useRecoilState(showSetupHcManagerState) + const [showRedeemAccount, setShowRedeemAccount] = useRecoilState(showRedeemAccountState) + + const router = useRouter() + const { account } = router.query + + const [hcManagerInfo, setHcManagerInfo] = useState(null) + const [user, setUser] = useState({ loggedIn: null }) + + useEffect(() => fcl.currentUser.subscribe(setUser), []) + + const { data: itemsData, error: itemsError } = useSWR( + account && isValidFlowAddress(account) ? ["hcManagerInfoFetcher", account] : null, hcManagerInfoFetcher + ) + + useEffect(() => { + if (itemsData) { + setHcManagerInfo(itemsData) + } + }, [itemsData]) + + if (!account) { + return
+ } + + if (!isValidFlowAddress(account)) { + return + } + + const showChildAccounts = () => { + if (!hcManagerInfo) { + return ( +
+ +
+ ) + } + + return ( + <> + {hcManagerInfo && hcManagerInfo.childAccounts.length > 0 ? + hcManagerInfo.childAccounts.map((item, index) => { + return ( + + ) + }) : +
+ Nothing found +
+ } + + ) + } + + const showOwnedAccounts = () => { + if (!hcManagerInfo) { + return ( +
+ +
+ ) + } + + return ( + <> + {hcManagerInfo && hcManagerInfo.ownedAccounts.length > 0 ? + hcManagerInfo.ownedAccounts.map((item, index) => { + return ( + + ) + }) : +
+ Nothing found +
+ } + + ) + } + + return ( +
+ +
+
+

+ {`HybridCustody Manager`} +

+
+ { + hcManagerInfo && !hcManagerInfo.isManagerExists ? + + : null + } + { + hcManagerInfo && hcManagerInfo.isManagerExists ? + + : null + } + { + hcManagerInfo && hcManagerInfo.isManagerExists ? + + : null + } +
+
+
+
+
+
+

+ {`ChildAccounts ${hcManagerInfo ? `(${hcManagerInfo.childAccounts.length})` : ""}`} +

+
+ {showChildAccounts()} +
+
+
+

+ {`OwnedAccounts ${hcManagerInfo ? `(${hcManagerInfo.ownedAccounts.length})` : ""}`} +

+
+ {showOwnedAccounts()} +
+
+
+ +
+
+
+
+ + + + +
+ ) +} \ No newline at end of file diff --git a/pages/account/[account]/hc/owned_account/index.js b/pages/account/[account]/hc/owned_account/index.js new file mode 100644 index 0000000..3612783 --- /dev/null +++ b/pages/account/[account]/hc/owned_account/index.js @@ -0,0 +1,174 @@ +import * as fcl from "@onflow/fcl" +import { useRouter } from "next/router" +import { useEffect, useState } from "react" +import useSWR from "swr" +import ItemsView from "../../../../../components/common/ItemsView" +import Layout from "../../../../../components/common/Layout" +import Spinner from "../../../../../components/common/Spinner" +import { isValidFlowAddress } from "../../../../../lib/utils" +import Custom404 from "../../404" +import { getOwnedAccountInfo } from "../../../../../flow/hc_scripts" +import { useRecoilState } from "recoil" +import { showPublishToParentState, showSetupDisplayState, showTransferOwnershipState, transactionInProgressState, transactionStatusState } from "../../../../../lib/atoms" +import SetupDisplayModal from "../../../../../components/hybrid_custody/SetupDisplayModal" +import PublishToParentModal from "../../../../../components/hybrid_custody/PublishToParentModal" +import { setupOwnedAccount } from "../../../../../flow/hc_transactions" +import ParentView from "../../../../../components/hybrid_custody/ParentView" +import TransferOwnershipModal from "../../../../../components/hybrid_custody/TransferOwnerShipModal" +import publicConfig from "../../../../../publicConfig" +import OwnedDisplayView from "../../../../../components/hybrid_custody/OwnedDisplayView" + +const ownedAccountInfoFetcher = async (funcName, address) => { + return getOwnedAccountInfo(address) +} + +export default function HybridCustodyOwnedAcct(props) { + const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState) + const [, setTransactionStatus] = useRecoilState(transactionStatusState) + const [showSetupDisplay, setShowSetupDisplay] = useRecoilState(showSetupDisplayState) + const [showPublishToParent, setShowPublishToParent] = useRecoilState(showPublishToParentState) + const [showTransferOwnership, setShowTransferOwnership] = useRecoilState(showTransferOwnershipState) + + const router = useRouter() + const { account } = router.query + + const [ownedAccountInfo, setOwnedAccountInfo] = useState(null) + const [user, setUser] = useState({ loggedIn: null }) + + useEffect(() => fcl.currentUser.subscribe(setUser), []) + + const { data: itemsData, error: itemsError } = useSWR( + account && isValidFlowAddress(account) ? ["ownedAccountInfoFetcher", account] : null, ownedAccountInfoFetcher + ) + + useEffect(() => { + if (itemsData) { + setOwnedAccountInfo(itemsData) + } + }, [itemsData]) + + if (!account) { + return
+ } + + if (!isValidFlowAddress(account)) { + return + } + + const showItems = () => { + if (!ownedAccountInfo) { + return ( +
+ +
+ ) + } + + return ( + <> + {ownedAccountInfo && ownedAccountInfo.parents.length > 0 ? + ownedAccountInfo.parents.map((item, index) => { + return ( + + ) + }) : +
+ Nothing found +
+ } + + ) + } + + return ( +
+ +
+
+
+

+ {`OwnedAccount`} +

+ { + ownedAccountInfo && ownedAccountInfo.owner ? + + : null} +
+ + +
+ { + ownedAccountInfo && !ownedAccountInfo.isOwnedAccountExists ? + + : null + } + { + ownedAccountInfo && ownedAccountInfo.isOwnedAccountExists ? + + : null + } + { + ownedAccountInfo && ownedAccountInfo.isOwnedAccountExists ? + + : null + } +
+
+
+
+
+ { + ownedAccountInfo && ownedAccountInfo.display ? +
+ +
: null + } +

+ {`Parents ${ownedAccountInfo ? `(${ownedAccountInfo.parents.length})` : ""}`} +

+
+ {showItems()} +
+
+
+
+
+
+ + + +
+ ) +} \ No newline at end of file diff --git a/public/scripts/hc/get_hc_manager_info.cdc b/public/scripts/hc/get_hc_manager_info.cdc new file mode 100644 index 0000000..f5638f7 --- /dev/null +++ b/public/scripts/hc/get_hc_manager_info.cdc @@ -0,0 +1,79 @@ +import HybridCustody from 0xHybridCustody +import MetadataViews from 0xMetadataViews + +pub struct ChildAccountInfo { + pub let address: Address + pub let display: MetadataViews.Display? + + init( + address: Address, + display: MetadataViews.Display? + ) { + self.address = address + self.display = display + } +} + +pub struct ManagerInfo { + pub let childAccounts: [ChildAccountInfo] + pub let ownedAccounts: [ChildAccountInfo] + pub let isManagerExists: Bool + + init( + childAccounts: [ChildAccountInfo], + ownedAccounts: [ChildAccountInfo], + isManagerExists: Bool + ) { + self.childAccounts = childAccounts + self.ownedAccounts = ownedAccounts + self.isManagerExists = isManagerExists + } +} + +pub fun main(child: Address): ManagerInfo { + let acct = getAuthAccount(child) + let m = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + + if let manager = m { + return ManagerInfo( + childAccounts: getChildAccounts(manager: manager), + ownedAccounts: getOwnedAccounts(manager: manager), + isManagerExists: true + ) + } + + return ManagerInfo( + childAccounts: [], + ownedAccounts: [], + isManagerExists: false + ) +} + +pub fun getChildAccounts(manager: &HybridCustody.Manager): [ChildAccountInfo] { + let childAddresses = manager.getChildAddresses() + let children: [ChildAccountInfo] = [] + for childAddress in childAddresses { + let display = manager.getChildAccountDisplay(address: childAddress) + let child = ChildAccountInfo(address: childAddress, display: display) + children.append(child) + } + + return children +} + +pub fun getOwnedAccounts(manager: &HybridCustody.Manager): [ChildAccountInfo] { + let ownedAddresses = manager.getOwnedAddresses() + let children: [ChildAccountInfo] = [] + for ownedAddress in ownedAddresses { + if let o = manager.borrowOwnedAccount(addr: ownedAddress) { + let d = o.resolveView(Type()) as? MetadataViews.Display? + if let display = d { + let child = ChildAccountInfo(address: ownedAddress, display: display) + children.append(child) + } + } else { + children.append(ChildAccountInfo(address: ownedAddress, display: nil)) + } + } + return children +} \ No newline at end of file diff --git a/public/scripts/hc/get_owned_account_info.cdc b/public/scripts/hc/get_owned_account_info.cdc new file mode 100644 index 0000000..a878258 --- /dev/null +++ b/public/scripts/hc/get_owned_account_info.cdc @@ -0,0 +1,88 @@ +import HybridCustody from 0xHybridCustody +import MetadataViews from 0xMetadataViews +import CapabilityFactory from 0xCapabilityFactory +import CapabilityFilter from 0xCapabilityFilter + +pub struct OwnedAccountInfo { + pub let display: MetadataViews.Display? + pub let parents: [ParentInfo] + pub let owner: Address? + pub let isOwnedAccountExists: Bool + + init( + display: MetadataViews.Display?, + parents: [ParentInfo], + owner: Address?, + isOwnedAccountExists: Bool + ) { + self.display = display + self.parents = parents + self.owner = owner + self.isOwnedAccountExists = isOwnedAccountExists + } +} + +pub struct ParentInfo { + pub let address: Address + pub let isClaimed: Bool + pub let childAccount: ChildAccountInfo? + + init( + address: Address, + isClaimed: Bool, + childAccount: ChildAccountInfo? + ) { + self.address = address + self.isClaimed = isClaimed + self.childAccount = childAccount + } +} + +pub struct ChildAccountInfo { + pub let factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}> + pub let filter: Capability<&{CapabilityFilter.Filter}> + + init( + factory: Capability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>, + filter: Capability<&{CapabilityFilter.Filter}> + ) { + self.factory = factory + self.filter = filter + } +} + +pub fun main(child: Address): OwnedAccountInfo { + let acct = getAuthAccount(child) + let o = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + if let owned = o { + let viewType = Type() + let display = owned.resolveView(viewType) as! MetadataViews.Display? + let parentAddresses = owned.getParentAddresses() + let parents: [ParentInfo] = [] + for parent in parentAddresses { + var childInfo: ChildAccountInfo? = nil + if let child = owned.borrowChildAccount(parent: parent) { + childInfo = ChildAccountInfo(factory: child.factory, filter: child.filter) + } + + let isClaimed = owned.getRedeemedStatus(addr: parent) ?? false + let p = ParentInfo(address: parent, isClaimed: isClaimed, childAccount: childInfo) + + parents.append(p) + } + + return OwnedAccountInfo( + display: display, + parents: parents, + owner: owned.getOwner(), + isOwnedAccountExists: true + ) + } + + return OwnedAccountInfo( + display: nil, + parents: [], + owner: nil, + isOwnedAccountExists: false + ) +} \ No newline at end of file diff --git a/public/transactions/hc/accept_ownership.cdc b/public/transactions/hc/accept_ownership.cdc new file mode 100644 index 0000000..cc11f95 --- /dev/null +++ b/public/transactions/hc/accept_ownership.cdc @@ -0,0 +1,19 @@ +#allowAccountLinking + +import HybridCustody from 0xHybridCustody +import MetadataViews from 0xMetadataViews + +transaction(childAddress: Address) { + prepare(acct: AuthAccount) { + let inboxName = HybridCustody.getOwnerIdentifier(acct.address) + let cap = acct.inbox.claim<&AnyResource{HybridCustody.OwnedAccountPrivate, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(inboxName, provider: childAddress) + ?? panic("owned account cap not found") + + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("manager no found") + + manager.addOwnedAccount(cap: cap) + } + + execute {} +} \ No newline at end of file diff --git a/public/transactions/hc/publish_to_parent.cdc b/public/transactions/hc/publish_to_parent.cdc new file mode 100644 index 0000000..8d1b871 --- /dev/null +++ b/public/transactions/hc/publish_to_parent.cdc @@ -0,0 +1,19 @@ +import HybridCustody from 0xHybridCustody +import CapabilityFactory from 0xCapabilityFactory +import CapabilityFilter from 0xCapabilityFilter +import CapabilityDelegator from 0xCapabilityDelegator + +transaction(parent: Address, factoryAddress: Address, filterAddress: Address) { + prepare(acct: AuthAccount) { + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + + let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath) + assert(factory.check(), message: "factory address is not configured properly") + + let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) + assert(filter.check(), message: "capability filter is not configured properly") + + owned.publishToParent(parentAddress: parent, factory: factory, filter: filter) + } +} \ No newline at end of file diff --git a/public/transactions/hc/redeem_account.cdc b/public/transactions/hc/redeem_account.cdc new file mode 100644 index 0000000..c0aca1e --- /dev/null +++ b/public/transactions/hc/redeem_account.cdc @@ -0,0 +1,17 @@ +import MetadataViews from 0xMetadataViews +import HybridCustody from 0xHybridCustody + +transaction(childAddress: Address) { + prepare(acct: AuthAccount) { + let inboxName = HybridCustody.getChildAccountIdentifier(acct.address) + let cap = acct.inbox.claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(inboxName, provider: childAddress) + ?? panic("child account cap not found") + + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("manager no found") + + manager.addAccount(cap: cap) + } + + execute {} +} \ No newline at end of file diff --git a/public/transactions/hc/remove_child_account.cdc b/public/transactions/hc/remove_child_account.cdc new file mode 100644 index 0000000..2ee3188 --- /dev/null +++ b/public/transactions/hc/remove_child_account.cdc @@ -0,0 +1,9 @@ +import HybridCustody from 0xHybridCustody + +transaction(child: Address) { + prepare (acct: AuthAccount) { + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("manager not found") + manager.removeChild(addr: child) + } +} \ No newline at end of file diff --git a/public/transactions/hc/remove_parent_from_child.cdc b/public/transactions/hc/remove_parent_from_child.cdc new file mode 100644 index 0000000..94a1628 --- /dev/null +++ b/public/transactions/hc/remove_parent_from_child.cdc @@ -0,0 +1,15 @@ +import HybridCustody from 0xHybridCustody + +transaction(parent: Address) { + prepare(acct: AuthAccount) { + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned not found") + + owned.removeParent(parent: parent) + + let manager = getAccount(parent).getCapability<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(HybridCustody.ManagerPublicPath) + .borrow() ?? panic("manager not found") + let children = manager.getChildAddresses() + assert(!children.contains(acct.address), message: "removed child is still in manager resource") + } +} \ No newline at end of file diff --git a/public/transactions/hc/setup_child_display.cdc b/public/transactions/hc/setup_child_display.cdc new file mode 100644 index 0000000..fda6d03 --- /dev/null +++ b/public/transactions/hc/setup_child_display.cdc @@ -0,0 +1,17 @@ +import HybridCustody from 0xHybridCustody +import MetadataViews from 0xMetadataViews + +transaction(childAddress: Address, name: String?, desc: String?, thumbnailURL: String?) { + prepare(acct: AuthAccount) { + let m = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("manager not found") + + if name != nil && desc != nil && thumbnailURL != nil { + let thumbnail = MetadataViews.HTTPFile(url: thumbnailURL!) + let display = MetadataViews.Display(name: name!, description: desc!, thumbnail: thumbnail!) + m.setChildAccountDisplay(address: childAddress, display) + } else { + panic("invalid params") + } + } +} \ No newline at end of file diff --git a/public/transactions/hc/setup_hc_manager.cdc b/public/transactions/hc/setup_hc_manager.cdc new file mode 100644 index 0000000..cccce3d --- /dev/null +++ b/public/transactions/hc/setup_hc_manager.cdc @@ -0,0 +1,24 @@ +import HybridCustody from 0xHybridCustody +import CapabilityFilter from 0xCapabilityFilter + +transaction(filterAddress: Address?, filterPath: String?) { + prepare(acct: AuthAccount) { + var filter: Capability<&{CapabilityFilter.Filter}>? = nil + if filterAddress != nil && filterPath != nil { + let path = PublicPath(identifier: filterPath!) + filter = getAccount(filterAddress!).getCapability<&{CapabilityFilter.Filter}>(path!) + } + + if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil { + let m <- HybridCustody.createManager(filter: filter) + acct.save(<- m, to: HybridCustody.ManagerStoragePath) + } + + acct.unlink(HybridCustody.ManagerPublicPath) + acct.unlink(HybridCustody.ManagerPrivatePath) + + acct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(HybridCustody.ManagerPrivatePath, target: HybridCustody.ManagerStoragePath) + acct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(HybridCustody.ManagerPublicPath, target: HybridCustody.ManagerStoragePath) + } +} + \ No newline at end of file diff --git a/public/transactions/hc/setup_owned_account.cdc b/public/transactions/hc/setup_owned_account.cdc new file mode 100644 index 0000000..af91916 --- /dev/null +++ b/public/transactions/hc/setup_owned_account.cdc @@ -0,0 +1,41 @@ +#allowAccountLinking + +import MetadataViews from 0xMetadataViews + +import HybridCustody from 0xHybridCustody + +/// This transaction configures an OwnedAccount in the signer if needed and configures its Capabilities per +/// HybridCustody's intended design. If Display values are specified (as recommended), they will be set on the +/// signer's OwnedAccount. +/// +transaction(name: String?, desc: String?, thumbnailURL: String?) { + prepare(acct: AuthAccount) { + var acctCap = acct.getCapability<&AuthAccount>(HybridCustody.LinkedAccountPrivatePath) + if !acctCap.check() { + acctCap = acct.linkAccount(HybridCustody.LinkedAccountPrivatePath)! + } + + if acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil { + let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap) + acct.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath) + } + + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + + // Set the display metadata for the OwnedAccount + if name != nil && desc != nil && thumbnailURL != nil { + let thumbnail = MetadataViews.HTTPFile(url: thumbnailURL!) + let display = MetadataViews.Display(name: name!, description: desc!, thumbnail: thumbnail!) + owned.setDisplay(display) + } + + // check that paths are all configured properly + acct.unlink(HybridCustody.OwnedAccountPrivatePath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPrivatePath, target: HybridCustody.OwnedAccountStoragePath) + + acct.unlink(HybridCustody.OwnedAccountPublicPath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPublicPath, target: HybridCustody.OwnedAccountStoragePath) + } +} + \ No newline at end of file diff --git a/public/transactions/hc/setup_owned_account_and_publish_to_parent.cdc b/public/transactions/hc/setup_owned_account_and_publish_to_parent.cdc new file mode 100644 index 0000000..4ec6a92 --- /dev/null +++ b/public/transactions/hc/setup_owned_account_and_publish_to_parent.cdc @@ -0,0 +1,61 @@ +#allowAccountLinking + +import MetadataViews from 0xMetadataViews + +import HybridCustody from 0xHybridCustody +import CapabilityFactory from 0xCapabilityFactory +import CapabilityFilter from 0xCapabilityFilter +import CapabilityDelegator from 0xCapabilityDelegator + +/// This transaction configures an OwnedAccount in the signer if needed, and proceeds to create a ChildAccount +/// using CapabilityFactory.Manager and CapabilityFilter.Filter Capabilities from the given addresses. A +/// Capability on the ChildAccount is then published to the specified parent account. +/// +transaction( + parent: Address, + factoryAddress: Address, + filterAddress: Address, + name: String?, + desc: String?, + thumbnailURL: String? + ) { + + prepare(acct: AuthAccount) { + // Configure OwnedAccount if it doesn't exist + if acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil { + var acctCap = acct.getCapability<&AuthAccount>(HybridCustody.LinkedAccountPrivatePath) + if !acctCap.check() { + acctCap = acct.linkAccount(HybridCustody.LinkedAccountPrivatePath)! + } + let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap) + acct.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath) + } + + // check that paths are all configured properly + acct.unlink(HybridCustody.OwnedAccountPrivatePath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPrivatePath, target: HybridCustody.OwnedAccountStoragePath) + + acct.unlink(HybridCustody.OwnedAccountPublicPath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPublicPath, target: HybridCustody.OwnedAccountStoragePath) + + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + + // Set the display metadata for the OwnedAccount + if name != nil && desc != nil && thumbnailURL != nil { + let thumbnail = MetadataViews.HTTPFile(url: thumbnailURL!) + let display = MetadataViews.Display(name: name!, description: desc!, thumbnail: thumbnail!) + owned.setDisplay(display) + } + + // Get CapabilityFactory & CapabilityFilter Capabilities + let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath) + assert(factory.check(), message: "factory address is not configured properly") + + let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) + assert(filter.check(), message: "capability filter is not configured properly") + + // Finally publish a ChildAccount capability on the signing account to the specified parent + owned.publishToParent(parentAddress: parent, factory: factory, filter: filter) + } +} \ No newline at end of file diff --git a/public/transactions/hc/transfer_ownership.cdc b/public/transactions/hc/transfer_ownership.cdc new file mode 100644 index 0000000..66536f6 --- /dev/null +++ b/public/transactions/hc/transfer_ownership.cdc @@ -0,0 +1,11 @@ +#allowAccountLinking + +import HybridCustody from 0xHybridCustody + +transaction(owner: Address) { + prepare(acct: AuthAccount) { + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned not found") + owned.giveOwnership(to: owner) + } +} \ No newline at end of file diff --git a/public/transactions/hc/transfer_ownership_from_manager.cdc b/public/transactions/hc/transfer_ownership_from_manager.cdc new file mode 100644 index 0000000..f216d17 --- /dev/null +++ b/public/transactions/hc/transfer_ownership_from_manager.cdc @@ -0,0 +1,11 @@ +#allowAccountLinking + +import HybridCustody from 0xHybridCustody + +transaction(ownedAddress: Address, owner: Address) { + prepare(acct: AuthAccount) { + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("manager not found") + manager.giveOwnership(addr: ownedAddress, to: owner) + } +} \ No newline at end of file diff --git a/publicConfig.js b/publicConfig.js index cb82d85..0c92f40 100644 --- a/publicConfig.js +++ b/publicConfig.js @@ -61,6 +61,9 @@ if (!linkURL) throw "Missing NEXT_PUBLIC_LINK_URL" const nftStorefrontV2Address = process.env.NEXT_PUBLIC_NFTSTOREFRONTV2_ADDRESS if (!nftStorefrontV2Address) throw "Missing NEXT_PUBLIC_NFTSTOREFRONTV2_ADDRESS" +const hybridCustodyAddress = process.env.NEXT_PUBLIC_HYBRIDCUSTODY_ADDRESS +if (!hybridCustodyAddress) throw "Missing NEXT_PUBLIC_HYBRIDCUSTODY_ADDRESS" + const publicConfig = { chainEnv, accessNodeAPI, @@ -77,6 +80,7 @@ const publicConfig = { flowboxAddress, accountBookmarkAddress, nftStorefrontV2Address, + hybridCustodyAddress, flownsURL, findURL, bayouURL,