diff --git a/packages/atlas/package.json b/packages/atlas/package.json index 4ab698f616..9de455c796 100644 --- a/packages/atlas/package.json +++ b/packages/atlas/package.json @@ -39,7 +39,7 @@ "@hcaptcha/react-hcaptcha": "^1.4.4", "@hookform/resolvers": "^2.9.10", "@joystream/js": "^1.4.0", - "@joystream/metadata-protobuf": "2.8.1", + "@joystream/metadata-protobuf": "2.9.0", "@joystream/types": "4.3.0", "@livesession/sdk": "^1.1.4", "@loadable/component": "^5.15.2", diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx index df294c90c2..6b64ad732a 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx @@ -3,6 +3,7 @@ import { flushSync } from 'react-dom' import { CSSTransition, SwitchTransition } from 'react-transition-group' import { CrtDrawer, CrtDrawerProps } from '@/components/CrtDrawer' +import { CreateTokenSuccessModal } from '@/components/_crt/CreateTokenSuccessModal' import { useConfirmationModal } from '@/providers/confirmationModal' import { transitions } from '@/styles' @@ -36,6 +37,7 @@ type CreateTokenDrawerProps = { export const CreateTokenDrawer = ({ show, onClose }: CreateTokenDrawerProps) => { const [activeStep, setActiveStep] = useState(CREATE_TOKEN_STEPS.setup) + const [showSuccessModal, setShowSuccessModal] = useState(false) const formData = useRef(CREATOR_TOKEN_INITIAL_DATA) const [primaryButtonProps, setPrimaryButtonProps] = useState['primaryButton']>() @@ -106,53 +108,58 @@ export const CreateTokenDrawer = ({ show, onClose }: CreateTokenDrawerProps) => } return ( - onClose()} - actionBar={{ - isNoneCrypto: true, - primaryButton: primaryButtonProps ?? {}, - secondaryButton, - }} - preview={preview} - formWrapperRef={formRef} - > - - { - nodeRef.current?.addEventListener('transitionend', done, false) - }} - onEntered={() => setIsGoingBack(false)} - classNames={isGoingBack ? transitions.names.backwardSlideSwitch : transitions.names.forwardSlideSwitch} - > -
- {activeStep === CREATE_TOKEN_STEPS.setup && ( - { - formData.current = { ...formData.current, ...data } - setActiveStep(CREATE_TOKEN_STEPS.issuance) - }} - /> - )} - {activeStep === CREATE_TOKEN_STEPS.issuance && ( - { - formData.current = { ...formData.current, ...data } - setActiveStep(CREATE_TOKEN_STEPS.summary) - }} - /> - )} - {activeStep === CREATE_TOKEN_STEPS.summary && } -
-
-
-
+ <> + + onClose()} + actionBar={{ + isNoneCrypto: true, + primaryButton: primaryButtonProps ?? {}, + secondaryButton, + }} + preview={preview} + formWrapperRef={formRef} + > + + { + nodeRef.current?.addEventListener('transitionend', done, false) + }} + onEntered={() => setIsGoingBack(false)} + classNames={isGoingBack ? transitions.names.backwardSlideSwitch : transitions.names.forwardSlideSwitch} + > +
+ {activeStep === CREATE_TOKEN_STEPS.setup && ( + { + formData.current = { ...formData.current, ...data } + setActiveStep(CREATE_TOKEN_STEPS.issuance) + }} + /> + )} + {activeStep === CREATE_TOKEN_STEPS.issuance && ( + { + formData.current = { ...formData.current, ...data } + setActiveStep(CREATE_TOKEN_STEPS.summary) + }} + /> + )} + {activeStep === CREATE_TOKEN_STEPS.summary && ( + setShowSuccessModal(true)} /> + )} +
+
+
+
+ ) } diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx index 10abecdb0d..a14b5e3bfa 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx @@ -31,6 +31,7 @@ const accessOptions = [ caption: 'Only members on allowlist can own your token. ', icon: , value: false, + disabled: true, }, ] diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.tsx index 7e7209fbdb..529bc012d3 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.tsx @@ -176,7 +176,7 @@ export const TokenIssuanceStep = ({ const data = assuranceType === 'custom' ? generateChartData(Number(customCliff ?? 0), Number(customVesting ?? 0), firstPayout ? firstPayout : 0) - : generateChartData(...getDataBasedOnType(assuranceType)) + : generateChartData(...(getDataBasedOnType(assuranceType) as [number, number, number])) setPreview( diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts index 855950e685..03140a42f4 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts @@ -1,6 +1,8 @@ import { Datum } from '@nivo/line' import { z } from 'zod' +import { IssuanceStepForm } from '@/components/_crt/CreateTokenDrawer/CreateTokenDrawer.types' + export const assuranceOptions = [ { label: 'Secure', @@ -172,13 +174,15 @@ export const generateChartData = (cliffTime: number, vestingTime: number, firstP return data } -export const getDataBasedOnType = (type: 'secure' | 'safe' | 'risky'): [number, number, number] => { +export const getDataBasedOnType = (type: IssuanceStepForm['assuranceType']): [number, number, number] | null => { switch (type) { case 'secure': return [6, 12, 50] case 'safe': return [0, 6, 50] case 'risky': - return [0, 0, 0] + return [0, 0, 100] + case 'custom': + return null } } diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx index f74d7ee469..6992195cd9 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenSummaryStep.tsx @@ -8,10 +8,13 @@ import { Text } from '@/components/Text' import { Tooltip } from '@/components/Tooltip' import { CrtFormWrapper } from '@/components/_crt/CrtFormWrapper' import { useMountEffect } from '@/hooks/useMountEffect' +import { useFee, useJoystream } from '@/providers/joystream' +import { useTransaction } from '@/providers/transactions/transactions.hooks' +import { useUser } from '@/providers/user/user.hooks' import { sizes } from '@/styles' import { formatNumber } from '@/utils/number' -import { cliffOptions, vestingOptions } from './TokenIssuanceStep/TokenIssuanceStep.utils' +import { cliffOptions, getDataBasedOnType, vestingOptions } from './TokenIssuanceStep/TokenIssuanceStep.utils' import { CommonStepProps } from './types' const cliffBanner = ( @@ -27,10 +30,52 @@ const cliffBanner = ( /> ) -export const TokenSummaryStep = ({ setPrimaryButtonProps, form }: CommonStepProps) => { +const monthDurationToBlocks = (numberOfMonths: number) => numberOfMonths * 30 * 24 * 60 * 6 +export type TokenSummaryStepProps = { + onSuccess: () => void +} & CommonStepProps +export const TokenSummaryStep = ({ setPrimaryButtonProps, form, onSuccess }: TokenSummaryStepProps) => { + const { joystream, proxyCallback } = useJoystream() + const { channelId, memberId } = useUser() + const handleTransaction = useTransaction() + const { fullFee } = useFee('issueCreatorTokenTx') + const handleSubmitTx = async () => { + if (!joystream || !channelId || !memberId) return + const [cliff, vesting, payout] = getDataBasedOnType(form.assuranceType) ?? [ + form.cliff, + form.vesting, + form.firstPayout, + ] + return handleTransaction({ + fee: fullFee, + txFactory: async (handleUpdate) => + (await joystream.extrinsics).issueCreatorToken( + memberId, + channelId, + form.name, + form.creatorReward, + form.revenueShare, + { + amount: String(form.creatorIssueAmount ?? 0), + cliffAmountPercentage: payout ?? 0, + vestingDuration: vesting ? monthDurationToBlocks(+vesting) : 0, + blocksBeforeCliff: cliff ? monthDurationToBlocks(+cliff) : 0, + }, + proxyCallback(handleUpdate) + ), + onTxSync: async () => { + onSuccess() + }, + snackbarSuccessMessage: { + title: `$${form.name} minted successfuly.`, + }, + }) + } + useMountEffect(() => { setPrimaryButtonProps({ text: 'Create token', + onClick: handleSubmitTx, }) }) @@ -142,7 +187,7 @@ export const TokenSummaryStep = ({ setPrimaryButtonProps, form }: CommonStepProp title="Transaction fee" tooltipText="This action requires a blockchain transaction, which comes with a fee." > - + diff --git a/packages/atlas/src/components/_crt/CreateTokenSuccessModal/CreateTokenSuccessModal.tsx b/packages/atlas/src/components/_crt/CreateTokenSuccessModal/CreateTokenSuccessModal.tsx new file mode 100644 index 0000000000..e8ecb7c968 --- /dev/null +++ b/packages/atlas/src/components/_crt/CreateTokenSuccessModal/CreateTokenSuccessModal.tsx @@ -0,0 +1,236 @@ +import { keyframes } from '@emotion/react' +import styled from '@emotion/styled' +import { useNavigate } from 'react-router-dom' + +import { SvgActionNewTab } from '@/assets/icons' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { TextButton } from '@/components/_buttons/Button' +import { DialogModal } from '@/components/_overlays/DialogModal' +import { absoluteRoutes } from '@/config/routes' +import { cVar, media, sizes } from '@/styles' + +export type CreateTokenSuccessModalProps = { + show: boolean + tokenName: string + tokenId: string +} +export const CreateTokenSuccessModal = ({ tokenName, show }: CreateTokenSuccessModalProps) => { + const navigate = useNavigate() + return ( + navigate(absoluteRoutes.studio.crtDashboard()), + }} + additionalActionsNode={ + } + iconPlacement="right" + onClick={() => navigate(absoluteRoutes.studio.crtDashboard())} // todo add correct route + > + View your token page + + } + dividers + > + +
+ + + + Congratulations you just made (minted) your ${tokenName} token! + + + Help your buyers discover your token by customizing the token page and putting it ouy to market! + + + + ) +} + +const spin = keyframes` + 0% { + width: var(--coin-size); + box-shadow: + 0 0 0 var(--side-dark); + animation-timing-function: ease-in; + } + + 49.999% { + width: 0.1rem; + box-shadow: + 0.05rem 0 0 var(--side), + 0.1rem 0 0 var(--side), + 0.15rem 0 0 var(--side), + 0.2rem 0 0 var(--side), + 0.25rem 0 0 var(--side), + 0.3rem 0 0 var(--side), + 0.35rem 0 0 var(--side), + 0.4rem 0 0 var(--side), + 0.45rem 0 0 var(--side), + 0.5rem 0 0 var(--side), + 0.55rem 0 0 var(--side), + 0.6rem 0 0 var(--side), + 0.65rem 0 0 var(--side), + 0.7rem 0 0 var(--side), + 0.75rem 0 0 var(--side); + transform: translateX(-0.375rem); + background-color: var(--lowlight); + animation-timing-function: linear; + } + + 50.001% { + width: 0.1rem; + box-shadow: + -0.05rem 0 0 var(--side), + -0.1rem 0 0 var(--side), + -0.15rem 0 0 var(--side), + -0.2rem 0 0 var(--side), + -0.25rem 0 0 var(--side), + -0.3rem 0 0 var(--side), + -0.35rem 0 0 var(--side), + -0.4rem 0 0 var(--side), + -0.45rem 0 0 var(--side), + -0.5rem 0 0 var(--side), + -0.55rem 0 0 var(--side), + -0.6rem 0 0 var(--side), + -0.65rem 0 0 var(--side), + -0.7rem 0 0 var(--side), + -0.75rem 0 0 var(--side); + transform: translateX(0.375rem); + background-color: var(--lowlight); + animation-timing-function: ease-out; + } + + 100% { + width: var(--coin-size); + box-shadow: + 0 0 0 var(--side-dark); + } +} + +@keyframes flip { + 0% { + height: var(--coin-size); + box-shadow: + 0 0 0 var(--side-dark); + animation-timing-function: ease-in; + } + + 49.999% { + height: 0.1rem; + box-shadow: + 0 0.05rem 0 var(--side), + 0 0.1rem 0 var(--side), + 0 0.15rem 0 var(--side), + 0 0.2rem 0 var(--side), + 0 0.25rem 0 var(--side), + 0 0.3rem 0 var(--side), + 0 0.35rem 0 var(--side), + 0 0.4rem 0 var(--side), + 0 0.45rem 0 var(--side), + 0 0.5rem 0 var(--side), + 0 0.55rem 0 var(--side), + 0 0.6rem 0 var(--side), + 0 0.65rem 0 var(--side), + 0 0.7rem 0 var(--side), + 0 0.75rem 0 var(--side); + transform: translateY(-0.375rem); + background-color: var(--lowlight); + animation-timing-function: linear; + } + + 50.001% { + height: 0.1rem; + box-shadow: + 0 -0.05rem 0 var(--side), + 0 -0.1rem 0 var(--side), + 0 -0.15rem 0 var(--side), + 0 -0.2rem 0 var(--side), + 0 -0.25rem 0 var(--side), + 0 -0.3rem 0 var(--side), + 0 -0.35rem 0 var(--side), + 0 -0.4rem 0 var(--side), + 0 -0.45rem 0 var(--side), + 0 -0.5rem 0 var(--side), + 0 -0.55rem 0 var(--side), + 0 -0.6rem 0 var(--side), + 0 -0.65rem 0 var(--side), + 0 -0.7rem 0 var(--side), + 0 -0.75rem 0 var(--side); + transform: translateY(0.375rem); + background-color: var(--lowlight); + animation-timing-function: ease-out; + } + + 100% { + height: var(--coin-size); + box-shadow: + 0 0 0 var(--side-dark); + } +` +export const IllustrationWrapper = styled.div` + margin: calc(var(--local-size-dialog-padding) * -1) calc(var(--local-size-dialog-padding) * -1) ${sizes(6)} + calc(var(--local-size-dialog-padding) * -1); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; + background-color: ${cVar('colorBackground')}; + padding: ${sizes(16)}; + + --face: #be9d66; + --lowlight: #111; + --side: #896c3b; + --side-dark: #120e08; + --coin-size: 9rem; + --coin-face: url('https://i.ibb.co/mCKfp8Q/Avatar.png'); + + .coin { + height: var(--coin-size); + width: var(--coin-size); + margin: 0.5rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + .coin::before { + content: ''; + display: block; + position: relative; + height: var(--coin-size); + width: var(--coin-size); + border-radius: 50%; + background-color: var(--face); + animation: ${spin} 3s linear infinite; + background-image: var(--coin-face); + background-size: 100% 100%; + background-position: center; + background-blend-mode: overlay; + } + + .coin.copper::before { + filter: hue-rotate(-40deg) brightness(0.75) contrast(115%); + animation-delay: -0.25s; + } + + .coin.silver::before { + filter: saturate(0); + animation-delay: -0.5s; + } + + > * { + width: 100%; + height: 208px; + + ${media.sm} { + height: 264px; + } + } +` diff --git a/packages/atlas/src/components/_crt/CreateTokenSuccessModal/index.ts b/packages/atlas/src/components/_crt/CreateTokenSuccessModal/index.ts new file mode 100644 index 0000000000..e690efbc83 --- /dev/null +++ b/packages/atlas/src/components/_crt/CreateTokenSuccessModal/index.ts @@ -0,0 +1 @@ +export * from './CreateTokenSuccessModal' diff --git a/packages/atlas/src/joystream-lib/extrinsics.ts b/packages/atlas/src/joystream-lib/extrinsics.ts index 28ff9f6c88..989a5b6f00 100644 --- a/packages/atlas/src/joystream-lib/extrinsics.ts +++ b/packages/atlas/src/joystream-lib/extrinsics.ts @@ -29,6 +29,7 @@ import { parseChannelExtrinsicInput, parseMemberExtrinsicInput, parseVideoExtrinsicInput, + prepareCreatorTokenMetadata, wrapMetadata, } from './metadata' import { @@ -1112,30 +1113,37 @@ export class JoystreamLibExtrinsics { channelId: ChannelId, symbol: string, patronageRate: number, + revenueSplitRate: number, initialCreatorAllocation: { amount: StringifiedNumber vestingDuration: number blocksBeforeCliff: number cliffAmountPercentage: number - }, - revenueSplitRate: number + } ) => { const member = createType('PalletContentPermissionsContentActor', { Member: parseInt(memberId) }) - const params = createType('PalletProjectTokenTokenIssuanceParameters', { - initialAllocation: createType('BTreeMap', { - [parseInt(memberId)]: createType('PalletProjectTokenTokenAllocation', { - amount: createType('u128', new BN(initialCreatorAllocation.amount)), - vestingScheduleParams: createType('Option', { - blocksBeforeCliff: createType('u32', new BN(initialCreatorAllocation.blocksBeforeCliff)), - linearVestingDuration: createType('u32', new BN(initialCreatorAllocation.vestingDuration)), - cliffAmountPercentage: initialCreatorAllocation.cliffAmountPercentage, - }), + const initialAllocation = createType('BTreeMap', new Map()) + initialAllocation.set( + createType('u64', new BN(memberId)), + createType('PalletProjectTokenTokenAllocation', { + amount: createType('u128', new BN(initialCreatorAllocation.amount)), + vestingScheduleParams: createType('Option', { + blocksBeforeCliff: createType('u32', new BN(initialCreatorAllocation.blocksBeforeCliff)), + linearVestingDuration: createType('u32', new BN(initialCreatorAllocation.vestingDuration)), + cliffAmountPercentage: createType( + 'Permill', + new BN(initialCreatorAllocation.cliffAmountPercentage) + ) as number, }), - }), - symbol, - patronageRate, - revenueSplitRate, - transferPolicy: createType('PalletProjectTokenTransferPolicyParams', { Permissionless: null }), + }) + ) + + const params = createType('PalletProjectTokenTokenIssuanceParameters', { + initialAllocation, + patronageRate: createType('Perquintill', patronageRate) as number, + revenueSplitRate: createType('Permill', revenueSplitRate) as number, + transferPolicy: createType('PalletProjectTokenTransferPolicyParams', 'Permissionless'), + metadata: prepareCreatorTokenMetadata({ symbol }), }) return this.api.tx.content.issueCreatorToken(member, parseInt(channelId), params) } diff --git a/packages/atlas/src/joystream-lib/metadata.ts b/packages/atlas/src/joystream-lib/metadata.ts index 42bad47ae5..63af70b3ab 100644 --- a/packages/atlas/src/joystream-lib/metadata.ts +++ b/packages/atlas/src/joystream-lib/metadata.ts @@ -6,8 +6,10 @@ import { IMediaType, IMembershipMetadata, IPublishedBeforeJoystream, + ITokenMetadata, IVideoMetadata, MembershipMetadata, + TokenMetadata, } from '@joystream/metadata-protobuf' import { createType } from '@joystream/types' import { ApiPromise as PolkadotApi } from '@polkadot/api' @@ -229,3 +231,11 @@ export const parseMemberExtrinsicInput: ParseExtrinsicInputFn { + const uInt8AMetadata = TokenMetadata.encode(metadata).finish() + + const metadataRaw = createType('Raw', uInt8AMetadata) + const metadataBytes = createType('Bytes', metadataRaw) + return createType('Bytes', metadataBytes) +} diff --git a/yarn.lock b/yarn.lock index 0c471f4f8e..cb1e5b2b10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4254,7 +4254,7 @@ __metadata: "@hcaptcha/react-hcaptcha": ^1.4.4 "@hookform/resolvers": ^2.9.10 "@joystream/js": ^1.4.0 - "@joystream/metadata-protobuf": 2.8.1 + "@joystream/metadata-protobuf": 2.9.0 "@joystream/prettier-config": ^1.0.0 "@joystream/types": 4.3.0 "@livesession/sdk": ^1.1.4 @@ -4384,7 +4384,23 @@ __metadata: languageName: node linkType: hard -"@joystream/metadata-protobuf@npm:2.8.1, @joystream/metadata-protobuf@npm:^2.8.1": +"@joystream/metadata-protobuf@npm:2.9.0": + version: 2.9.0 + resolution: "@joystream/metadata-protobuf@npm:2.9.0" + dependencies: + "@types/iso-3166-2": ^1.0.0 + "@types/long": ^4.0.1 + google-protobuf: ^3.14.0 + i18n-iso-countries: ^6.8.0 + iso-3166-2: ^1.0.0 + iso-639-1: ^2.1.9 + long: ^4.0.0 + protobufjs: ^6.11.2 + checksum: 790392c16853b79fed43a3c49a5f473ce12076ca54b1853aa5b3e7f9b6effbb4103e3e68d96aae0a5c6c6245f359439278bf6fac5d6f0f80793d06a382abb52a + languageName: node + linkType: hard + +"@joystream/metadata-protobuf@npm:^2.8.1": version: 2.8.1 resolution: "@joystream/metadata-protobuf@npm:2.8.1" dependencies: