Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: multi target bulk send #12

Merged
merged 2 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions components/bookmark/AccountBookmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import {
transactionStatusState,
showNoteEditorState,
accountBookmarkState,
transactionInProgressState
transactionInProgressState,
showBasicNotificationState,
basicNotificationContentState
} from "../../lib/atoms"
import { removeAccountBookmark } from "../../flow/bookmark_transactions";
import { PencilAltIcon } from "@heroicons/react/outline";
import { DocumentDuplicateIcon, PencilAltIcon } from "@heroicons/react/outline";

export default function AccountBookmark(props) {
const [, setShowBasicNotification] = useRecoilState(showBasicNotificationState)
const [, setBasicNotificationContent] = useRecoilState(basicNotificationContentState)
const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState)
const [, setTransactionStatus] = useRecoilState(transactionStatusState)
const [showNoteEditor, setShowNoteEditor] = useRecoilState(showNoteEditorState)
Expand All @@ -36,14 +40,23 @@ export default function AccountBookmark(props) {
}, undefined, { shallow: true })
}}
>{bookmark.address}</div>
<SolidStar className="text-yellow-400 w-6 h-6 cursor-pointer"
onClick={async () => {
if (transactionInProgress) {
return
}
await removeAccountBookmark(bookmark.address, setTransactionInProgress, setTransactionStatus)
mutate(["accountBookmarksFetcher", user.addr])
}}/>
<div className="flex gap-x-1 items-center justify-center">
<DocumentDuplicateIcon className="cursor-pointer text-gray-700 hover:text-drizzle w-5 h-5"
onClick={async () => {
await navigator.clipboard.writeText(bookmark.address)
setShowBasicNotification(true)
setBasicNotificationContent({ type: "information", title: "Copied!", detail: null })
}} />
<SolidStar className="text-yellow-400 w-6 h-6 cursor-pointer"
onClick={async () => {
if (transactionInProgress) {
return
}
await removeAccountBookmark(bookmark.address, setTransactionInProgress, setTransactionStatus)
mutate(["accountBookmarksFetcher", user.addr])
}} />
</div>

</div>
<div className="flex gap-x-2 items-center justify-between">
<div>{bookmark.note}</div>
Expand Down
12 changes: 10 additions & 2 deletions components/collection/NftBulkTransferModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function NftBulkTransferModal(props) {
<div className="mt-3">
<Dialog.Title as="h3" className="text-xl leading-6 font-bold text-gray-900 mb-4">
{showNftBulkTransfer.mode == "NftBulkTransfer" ?
"NFT Bulk Transfer" : "Unknown"}
"NFT Bulk Transfer" : "Set Recipient"}
</Dialog.Title>
<div className='flex flex-col gap-y-4'>
<div>
Expand Down Expand Up @@ -118,8 +118,16 @@ export default function NftBulkTransferModal(props) {
storagePath, publicPath,
setTransactionInProgress, setTransactionStatus
)
router.reload()
} else if (showNftBulkTransfer.mode == "SetRecipient") {
let tokens = Object.assign({}, selectedTokens)
for (const [tokenId, properties] of Object.entries(tokens)) {
if (properties.isSelected && !properties.recipient) {
properties.recipient = recipient
}
}
setSelectedTokens(tokens)
}
router.reload()
}}
>
{"Confirm"}
Expand Down
176 changes: 176 additions & 0 deletions components/collection/NftBulkTransferPreviewModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { Fragment, useEffect, useRef, useState } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { useRecoilState } from "recoil"
import { transactionStatusState, transactionInProgressState, showNftBulkTransferPreviewState } from '../../lib/atoms'
import * as fcl from "@onflow/fcl";
import Image from "next/image"
import { getRarityColor, isValidFlowAddress, isValidPositiveFlowDecimals, isValidPositiveNumber, isValidUrl } from '../../lib/utils'
import { useRouter } from 'next/router'
import { useSWRConfig } from 'swr'
import { acceptOwnership, redeemAccount } from '../../flow/hc_transactions';
import { bulkTransferNft } from '../../flow/nft_transactions';

export default function NftBulkTransferPreviewModal(props) {
const router = useRouter()
const { mutate } = useSWRConfig()
const { account: account, collection: collectionPath } = router.query
const { selectedTokens, setSelectedTokens, collection } = props
const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState)
const [, setTransactionStatus] = useRecoilState(transactionStatusState)

const [showNftBulkTransferPreview, setShowNftBulkTransferPreview] = useRecoilState(showNftBulkTransferPreviewState)

useEffect(() => fcl.currentUser.subscribe(setUser), [])
const [user, setUser] = useState({ loggedIn: null })

const cancelButtonRef = useRef(null)

const getPreviewList = () => {
const tokenIds = Object.entries(selectedTokens).filter(([tokenId, properties]) => properties.isSelected && properties.recipient).map(([tokenId, selected]) => tokenId)
const previewList = tokenIds.map((tokenId) => {
const token = selectedTokens[tokenId]
return {
tokenId: tokenId,
recipient: token.recipient,
display: token.display
}
}).sort((a, b) => a.recipient.localeCompare(b.recipient))
return previewList
}

const getPreviewView = () => {
const previewList = getPreviewList()
return (
<div className='flex gap-x-3 p-4 overflow-auto'>
{
previewList.map((token, index) => {
const display = token.display
const rarityColor = getRarityColor(display.rarity ? display.rarity.toLowerCase() : null)
return (
<div key={`preview-${index}`} className='flex flex-col justify-between items-center gap-y-3 w-36'>
<div className={`w-36 text-center bg-green-100 text-green-800 rounded-xl font-flow font-medium text-xs`}>
{`#${index + 1}`}
</div>
<div className={
`ring-1 ring-drizzle w-36 h-60 bg-white rounded-2xl flex flex-col gap-y-1 pb-2 justify-between items-center shrink-0 overflow-hidden shadow-md`
}>
<div className="flex justify-center w-full rounded-t-2xl aspect-square bg-drizzle-ultralight relative overflow-hidden">
<Image className={"object-contain"} src={display.imageSrc || "/token_placeholder.png"} fill alt="" priority sizes="5vw" />
{
display.rarity ?
<div className={`absolute top-2 px-2 ${rarityColor} rounded-full font-flow font-medium text-xs`}>
{`${display.rarity}`.toUpperCase()}
</div> : null
}
</div>
<label className="px-3 max-h-12 break-words overflow-hidden text-ellipsis font-flow font-semibold text-xs text-black">
{`${display.name}`}
</label>
<div className="flex flex-col items-center justify-center">
<div className="flex gap-x-1 justify-center items-center">
{
display.transferrable == true ? null :
<div className={`w-4 h-4 text-center bg-indigo-100 text-indigo-800 rounded-full font-flow font-medium text-xs`}>
{"S"}
</div>
}
<label className="font-flow font-medium text-xs text-gray-400">
{`#${display.tokenID}`}
</label>
</div>

</div>
</div>
<div className={`px-1 text-center bg-indigo-100 text-indigo-800 rounded-xl font-flow font-medium text-sm`}>
{token.recipient}
</div>
</div>
)
})
}
</div>
)
}

return (
<Transition.Root show={showNftBulkTransferPreview.show} as={Fragment}>
<Dialog as="div" className="relative z-10" initialFocus={cancelButtonRef} onClose={() => setShowNftBulkTransferPreview(prev => ({
...prev, show: false
}))}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end sm:items-center justify-center min-h-full p-2 text-center sm:p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="min-w-[320px] relative bg-white rounded-2xl p-4 text-left overflow-hidden shadow-xl transform transition-all sm:max-w-3xl sm:w-full sm:p-6">
<div>
<div className="mt-3">
<Dialog.Title as="h3" className="text-xl leading-6 font-bold text-gray-900 mb-4">
NFT Bulk Transfer
</Dialog.Title>
<div className='flex flex-col gap-y-4'>
{getPreviewView()}
</div>
</div>
</div>
<div className="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
<button
type="button"
disabled={transactionInProgress}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-drizzle text-base font-medium text-black hover:bg-drizzle-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-drizzle sm:col-start-2 sm:text-sm disabled:bg-drizzle-light disabled:text-gray-500"
onClick={async () => {
setShowNftBulkTransferPreview(prev => ({
...prev, show: false
}))

const publicPath = `/public/${collection.publicPathIdentifier}`
const storagePath = `/storage/${collection.storagePathIdentifier}`
const list = getPreviewList()
const tokenIds = list.map((token) => token.tokenId)
const recipients = list.map((token) => token.recipient)
await bulkTransferNft(recipients, tokenIds,
storagePath, publicPath,
setTransactionInProgress, setTransactionStatus
)
router.reload()
}}
>
{"Confirm"}
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-drizzle sm:mt-0 sm:col-start-1 sm:text-sm"
onClick={() => setShowNftBulkTransferPreview(prev => ({
...prev, show: false
}))}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
6 changes: 3 additions & 3 deletions components/common/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default function Layout({ children }) {
return null
}
if (!bookmark) {
return <StarIcon className="text-gray-700 hover:text-drizzle w-6 h-6"
return <StarIcon className="cursor-pointer text-gray-700 hover:text-drizzle w-6 h-6"
onClick={async () => {
if (transactionInProgress) {
return
Expand All @@ -122,7 +122,7 @@ export default function Layout({ children }) {
/>
}

return <SolidStar className="text-yellow-400 w-6 h-6"
return <SolidStar className="cursor-pointer text-yellow-400 w-6 h-6"
onClick={async () => {
if (transactionInProgress) {
return
Expand All @@ -149,7 +149,7 @@ export default function Layout({ children }) {
<label className="text-lg sm:text-xl text-gray-500">Account</label>
<div className="flex gap-x-2 items-center">
<label className="text-2xl sm:text-3xl font-bold">{`${account}`}</label>
<DocumentDuplicateIcon className="text-gray-700 hover:text-drizzle w-6 h-6"
<DocumentDuplicateIcon className="cursor-pointer text-gray-700 hover:text-drizzle w-6 h-6"
onClick={async () => {
await navigator.clipboard.writeText(account)
setShowBasicNotification(true)
Expand Down
14 changes: 11 additions & 3 deletions components/common/NFTView.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default function NFTView(props) {
const router = useRouter()
const rarityColor = getRarityColor(display.rarity ? display.rarity.toLowerCase() : null)
const maxSelection = 20
console.log(display)
return (
<div className={
classNames(
Expand Down Expand Up @@ -41,10 +40,10 @@ export default function NFTView(props) {
setShowBasicNotification(true)
setBasicNotificationContent({ type: "exclamation", title: `You can select ${maxSelection} NFTs at most`, detail: null })
} else {
tokens[tokenId] = { isSelected: true, selectedAt: new Date().getTime(), display: display }
tokens[tokenId] = { isSelected: true, selectedAt: new Date().getTime(), display: display, recipient: null }
}
} else {
tokens[tokenId] = { isSelected: false, selectedAt: 0, display: display }
tokens[tokenId] = { isSelected: false, selectedAt: 0, display: display, recipient: null }
}
setSelectedTokens(tokens)
}
Expand All @@ -61,6 +60,7 @@ export default function NFTView(props) {
<label className="px-3 max-h-12 break-words overflow-hidden text-ellipsis font-flow font-semibold text-xs text-black">
{`${display.name}`}
</label>
<div className="flex flex-col items-center justify-center">
<div className="flex gap-x-1 justify-center items-center">
{
display.transferrable == true ? null :
Expand All @@ -72,6 +72,14 @@ export default function NFTView(props) {
{`#${display.tokenID}`}
</label>
</div>
{
selectMode == "Select" && selectedTokens[tokenId] && selectedTokens[tokenId].isSelected && selectedTokens[tokenId].recipient ?
<div className={`px-1 h-4 text-center bg-indigo-100 text-indigo-800 rounded-full font-flow font-medium text-xs`}>
{selectedTokens[tokenId].recipient}
</div>
: null
}
</div>
</div>
)
}
12 changes: 4 additions & 8 deletions flow/nft_transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,26 @@ const doTransferNft = async (address, tokenId, collectionStoragePath, collection
}

export const bulkTransferNft = async (
address, tokenIds, collectionStoragePath, collectionPublicPath,
recipients, tokenIds, collectionStoragePath, collectionPublicPath,
setTransactionInProgress,
setTransactionStatus
) => {
if (!isValidFlowAddress(address)) {
return
}

const txFunc = async () => {
return await doBulkTransferNft(address, tokenIds, collectionStoragePath, collectionPublicPath)
return await doBulkTransferNft(recipients, tokenIds, collectionStoragePath, collectionPublicPath)
}

return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
}

const doBulkTransferNft = async (address, tokenIds, collectionStoragePath, collectionPublicPath) => {
const doBulkTransferNft = async (recipients, tokenIds, collectionStoragePath, collectionPublicPath) => {
const rawCode = await (await fetch("/transactions/collection/bulk_transfer_nft.cdc")).text()
const code = rawCode.replace("__NFT_STORAGE_PATH__", collectionStoragePath)
.replace("__NFT_PUBLIC_PATH__", collectionPublicPath)

const transactionId = fcl.mutate({
cadence: code,
args: (arg, t) => [
arg(address, t.Address),
arg(recipients, t.Array(t.Address)),
arg(tokenIds, t.Array(t.UInt64))
],
proposer: fcl.currentUser,
Expand Down
1 change: 0 additions & 1 deletion flow/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export const getNftViews = async (address, storagePathID, tokenIDs) => {
]
})

console.log(displays)
return displays
}

Expand Down
7 changes: 6 additions & 1 deletion lib/atoms.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ export const showRedeemAccountState = atom({

export const showNftBulkTransferState = atom({
key: "showNftBulkTransferState",
default: {show: false, mode: "NftBulkTransfer"}
default: {show: false, mode: "SetTarget"}
})

export const showNftBulkTransferPreviewState = atom({
key: "showNftBulkTransferPreviewState",
default: {show: false, mode: "NftBulkTransferPreview"}
})

export const showTransferOwnershipState = atom({
Expand Down
Loading