StargazeTools init
14
.env.example
Normal file
@ -0,0 +1,14 @@
|
||||
APP_VERSION=0.1.0
|
||||
|
||||
NEXT_PUBLIC_CW721_BASE_CODE_ID=5
|
||||
|
||||
NEXT_PUBLIC_API_URL=https://
|
||||
NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
|
||||
NEXT_PUBLIC_NETWORK=testnet
|
||||
NEXT_PUBLIC_WEBSITE_URL=https://
|
||||
|
||||
NEXT_PUBLIC_S3_BUCKET= # TODO
|
||||
NEXT_PUBLIC_S3_ENDPOINT= # TODO
|
||||
NEXT_PUBLIC_S3_KEY= # TODO
|
||||
NEXT_PUBLIC_S3_REGION= # TODO
|
||||
NEXT_PUBLIC_S3_SECRET= # TODO
|
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
||||
* @orkunkl @findolor @kaymakf
|
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: '[BUG]'
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
23
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
Fixes #<ISSUE_NUMBER>
|
||||
|
||||
## Description
|
||||
|
||||
Describe big picture changes here. This will give viewers context for the changes.
|
||||
|
||||
## Changes
|
||||
|
||||
List any technical changes.
|
||||
|
||||
- [ ] Change 1
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
|
||||
## Testing Steps
|
||||
|
||||
As a reviewer, what steps should I take to verify this is working correctly?
|
||||
|
||||
- [ ] Step 1
|
||||
|
||||
## Links
|
||||
|
||||
Add links to Figma files, documentation, etc.
|
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
.env*
|
||||
.vercel
|
||||
.next/
|
||||
node_modules/
|
||||
out/
|
||||
|
||||
!.env.example
|
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
.next/**
|
||||
dist/**
|
||||
node_modules/**
|
||||
out/**
|
28
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"css.validate": false,
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.packageManager": "yarn",
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"!cwd": false,
|
||||
"pattern": "packages/*"
|
||||
},
|
||||
{
|
||||
"!cwd": false,
|
||||
"directory": "."
|
||||
}
|
||||
],
|
||||
"npm.packageManager": "yarn"
|
||||
}
|
53
README.md
Normal file
@ -0,0 +1,53 @@
|
||||
<!-- markdownlint-disable MD033 MD034 MD036 MD041 -->
|
||||
|
||||

|
||||
|
||||
# stargaze-tools
|
||||
|
||||
- Mainnet website: https://
|
||||
- Testnet website: https://
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Required**
|
||||
|
||||
- Git
|
||||
- Node.js 14 or LTS
|
||||
- Yarn
|
||||
- Keplr Wallet browser extension
|
||||
|
||||
**Optional**
|
||||
|
||||
- S3 bucket instance (minio, etc.)
|
||||
|
||||
## Setup local development
|
||||
|
||||
```sh
|
||||
# clone repository
|
||||
git clone https://github.com/deus-labs/stargaze-tools.git
|
||||
cd stargaze-tools
|
||||
|
||||
# install dependencies
|
||||
yarn install
|
||||
|
||||
# copy env file and fill in values
|
||||
cp .env.example .env
|
||||
|
||||
# run development server
|
||||
yarn dev
|
||||
|
||||
# (optional) lint and format project
|
||||
yarn lint
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- https://docs.stargaze.zone
|
||||
|
||||
## Questions
|
||||
|
||||
- [Discord](https://discord.gg/stargaze)
|
||||
- [Telegram](https://t.me/joinchat/ZQ95YmIn3AI0ODFh)
|
||||
- [Twitter](https://twitter.com/stargazezone)
|
||||
|
||||
<img src="./public/icon.png" height="96" align="right" />
|
42
components/Alert.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { FaExclamationTriangle, FaInfoCircle, FaTimes } from 'react-icons/fa'
|
||||
import type { IconType } from 'react-icons/lib'
|
||||
|
||||
export type AlertType = 'info' | 'warning' | 'error' | 'ghost'
|
||||
|
||||
const ALERT_ICONS_MAP: Record<AlertType, IconType | null> = {
|
||||
info: FaInfoCircle,
|
||||
warning: FaExclamationTriangle,
|
||||
error: FaTimes,
|
||||
ghost: null,
|
||||
}
|
||||
|
||||
export interface AlertProps extends ComponentProps<'div'> {
|
||||
type?: AlertType
|
||||
}
|
||||
|
||||
export const Alert = (props: AlertProps) => {
|
||||
const { type = 'info', className, children, ...rest } = props
|
||||
const AlertIcon = ALERT_ICONS_MAP[type]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative p-4 space-x-4 border-l-4',
|
||||
{ 'bg-blue-500/25 border-blue-500': type === 'info' },
|
||||
{ 'bg-yellow-500/25 border-yellow-500': type === 'warning' },
|
||||
{ 'bg-red-500/25 border-red-500': type === 'error' },
|
||||
{ 'bg-stone-500/25 border-stone-500': type === 'ghost' },
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{AlertIcon && <AlertIcon size={24} />}
|
||||
<div className="flex flex-col flex-grow space-y-2">
|
||||
{children}
|
||||
{/* */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
35
components/Anchor.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import Link from 'next/link'
|
||||
import type { ComponentProps, ComponentType } from 'react'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
export interface AnchorProps extends ComponentProps<'a'> {
|
||||
external?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Adaptive link component that can be used for both relative Next.js pages and
|
||||
* external links, recommended for sidebar and footer links
|
||||
*
|
||||
* @see https://nextjs.org/docs/api-reference/next/link
|
||||
*/
|
||||
export function Anchor({ children, external, href = '', rel = '', ...rest }: AnchorProps) {
|
||||
const isApi = href.startsWith('/api')
|
||||
const isRelative = href.startsWith('/')
|
||||
const isExternal = typeof external === 'boolean' ? external : isApi || !isRelative
|
||||
|
||||
const Wrap = (isExternal ? Fragment : Link) as ComponentType<any>
|
||||
const wrapProps = isExternal ? {} : { href }
|
||||
const linkProps = isExternal ? { target: '_blank', rel: `noopener noreferrer ${rel}` } : { rel }
|
||||
|
||||
return (
|
||||
<Wrap {...wrapProps}>
|
||||
<a {...rest} {...linkProps} href={href}>
|
||||
{children ?? (href ? trimHttp(href) : null)}
|
||||
</a>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
|
||||
function trimHttp(str: string) {
|
||||
return str.replace(/https?:\/\//, '')
|
||||
}
|
36
components/AnchorButton.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import clsx from 'clsx'
|
||||
import type { AnchorProps } from 'components/Anchor'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
|
||||
export type ButtonVariant = 'solid' | 'outline'
|
||||
|
||||
export interface AnchorButtonProps extends AnchorProps {
|
||||
isDisabled?: boolean
|
||||
isWide?: boolean
|
||||
leftIcon?: JSX.Element
|
||||
rightIcon?: JSX.Element
|
||||
variant?: ButtonVariant
|
||||
}
|
||||
|
||||
export const AnchorButton = (props: AnchorButtonProps) => {
|
||||
const { isWide, leftIcon, rightIcon, variant = 'solid', className, children, ...rest } = props
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
className={clsx(
|
||||
'group flex items-center py-2 space-x-2 font-bold focus:ring',
|
||||
isWide ? 'px-8' : 'px-4',
|
||||
{
|
||||
'bg-plumbus-60 hover:bg-plumbus-50 rounded ': variant === 'solid',
|
||||
'bg-plumbus/10 hover:bg-plumbus/20 rounded border border-plumbus': variant === 'outline',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{leftIcon}
|
||||
<div className="group-hover:underline">{children}</div>
|
||||
{rightIcon}
|
||||
</Anchor>
|
||||
)
|
||||
}
|
33
components/BrandColorPicker.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import clsx from 'clsx'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
export const BRAND_COLORS = ['plumbus', 'black', 'white'] as const
|
||||
export type BrandColor = typeof BRAND_COLORS[number]
|
||||
|
||||
export interface BrandColorPickerProps {
|
||||
onChange: Dispatch<SetStateAction<BrandColor>>
|
||||
}
|
||||
|
||||
export const BrandColorPicker = ({ onChange }: BrandColorPickerProps) => {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-bold">Change color:</span>
|
||||
{BRAND_COLORS.map((color) => (
|
||||
<button
|
||||
key={`change-color-${color}`}
|
||||
className={clsx(
|
||||
'w-8 h-8 rounded border border-white/20',
|
||||
'hover:ring-2 focus:ring-2 ring-white/50 transition',
|
||||
{
|
||||
'bg-plumbus': color === 'plumbus',
|
||||
'bg-black': color === 'black',
|
||||
'bg-white': color === 'white',
|
||||
},
|
||||
)}
|
||||
onClick={() => onChange(color)}
|
||||
type="button"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
53
components/BrandPreview.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import clsx from 'clsx'
|
||||
import type { BrandColor } from 'components/BrandColorPicker'
|
||||
import { BrandColorPicker } from 'components/BrandColorPicker'
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { FaDownload } from 'react-icons/fa'
|
||||
|
||||
export interface BrandPreviewProps {
|
||||
name: string
|
||||
id?: string
|
||||
url?: string
|
||||
Asset: ComponentType<SVGProps<SVGSVGElement>>
|
||||
}
|
||||
|
||||
export const BrandPreview = ({ name, id = '', url, Asset }: BrandPreviewProps) => {
|
||||
const [color, setColor] = useState<BrandColor>('plumbus')
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center space-x-4">
|
||||
<a className="text-2xl font-bold hover:underline" href={`#${id}`} id={id}>
|
||||
{name}
|
||||
</a>
|
||||
{url && (
|
||||
<a
|
||||
className={clsx(
|
||||
'flex items-center py-2 px-4 space-x-2 bg-plumbus-60 rounded',
|
||||
'hover:bg-plumbus-70 transition hover:translate-y-[-2px]',
|
||||
)}
|
||||
download
|
||||
href={`/${url}`}
|
||||
>
|
||||
<FaDownload />
|
||||
<span className="font-bold">Download SVG</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center items-center p-16 max-h-[400px] bg-black/20 rounded">
|
||||
<Asset
|
||||
className={clsx('transition', {
|
||||
'text-plumbus': color === 'plumbus',
|
||||
'text-black': color === 'black',
|
||||
'text-white': color === 'white',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<BrandColorPicker onChange={setColor} />
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
}
|
41
components/Button.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { CgSpinnerAlt } from 'react-icons/cg'
|
||||
|
||||
export type ButtonVariant = 'solid' | 'outline'
|
||||
|
||||
export interface ButtonProps extends ComponentProps<'button'> {
|
||||
isDisabled?: boolean
|
||||
isLoading?: boolean
|
||||
isWide?: boolean
|
||||
leftIcon?: JSX.Element
|
||||
rightIcon?: JSX.Element
|
||||
variant?: ButtonVariant
|
||||
}
|
||||
|
||||
export const Button = (props: ButtonProps) => {
|
||||
const { isDisabled, isLoading, isWide, leftIcon, rightIcon, variant = 'solid', className, children, ...rest } = props
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'group flex items-center py-2 space-x-2 font-bold focus:ring',
|
||||
isWide ? 'px-8' : 'px-4',
|
||||
{
|
||||
'bg-plumbus-60 hover:bg-plumbus-50 rounded ': variant === 'solid',
|
||||
'bg-plumbus/10 hover:bg-plumbus/20 rounded border border-plumbus': variant === 'outline',
|
||||
'opacity-50 cursor-not-allowed pointer-events-none': isDisabled,
|
||||
'animate-pulse cursor-wait pointer-events-none': isLoading,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
type="button"
|
||||
{...rest}
|
||||
>
|
||||
{isLoading ? <CgSpinnerAlt className="animate-spin" /> : leftIcon}
|
||||
<div>{children}</div>
|
||||
{!isLoading && rightIcon}
|
||||
</button>
|
||||
)
|
||||
}
|
11
components/Conditional.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface ConditionalProps {
|
||||
test: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const Conditional = ({ test, children }: ConditionalProps) => {
|
||||
if (!test) return null
|
||||
return <>{children}</>
|
||||
}
|
21
components/ContractPageHeader.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Anchor } from 'components/Anchor'
|
||||
|
||||
import { PageHeader } from './PageHeader'
|
||||
|
||||
interface ContractPageHeaderProps {
|
||||
title: string
|
||||
description?: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export const ContractPageHeader = ({ title, description, link }: ContractPageHeaderProps) => {
|
||||
return (
|
||||
<PageHeader title={title}>
|
||||
{description} Learn more in the{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href={link}>
|
||||
documentation
|
||||
</Anchor>
|
||||
.
|
||||
</PageHeader>
|
||||
)
|
||||
}
|
29
components/DefaultAppSeo.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { meta } from 'config/meta'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import { WEBSITE_URL } from 'utils/constants'
|
||||
|
||||
export const DefaultAppSeo = () => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<DefaultSeo
|
||||
canonical={meta.url + (router.asPath || '')}
|
||||
defaultTitle={meta.name}
|
||||
description={meta.description}
|
||||
openGraph={{
|
||||
title: meta.name,
|
||||
description: meta.description,
|
||||
type: 'website',
|
||||
site_name: meta.name,
|
||||
images: [{ url: `${WEBSITE_URL}/social.png` }],
|
||||
}}
|
||||
titleTemplate={`%s | ${meta.name}`}
|
||||
twitter={{
|
||||
cardType: 'summary_large_image',
|
||||
handle: meta.twitter.username,
|
||||
site: meta.twitter.username,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
162
components/FaviconsMetaTags.jsx
Normal file
@ -0,0 +1,162 @@
|
||||
export function FaviconsMetaTags() {
|
||||
return (
|
||||
<>
|
||||
<link href="/assets/favicon.ico" rel="shortcut icon" />
|
||||
<link href="/assets/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png" />
|
||||
<link href="/assets/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png" />
|
||||
<link href="/assets/favicon-48x48.png" rel="icon" sizes="48x48" type="image/png" />
|
||||
<link href="/assets/manifest.webmanifest" rel="manifest" />
|
||||
<meta content="yes" name="mobile-web-app-capable" />
|
||||
<meta content="#F0827D" name="theme-color" />
|
||||
<meta content="StargazeTools" name="application-name" />
|
||||
<link href="/assets/apple-touch-icon-57x57.png" rel="apple-touch-icon" sizes="57x57" />
|
||||
<link href="/assets/apple-touch-icon-60x60.png" rel="apple-touch-icon" sizes="60x60" />
|
||||
<link href="/assets/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72" />
|
||||
<link href="/assets/apple-touch-icon-76x76.png" rel="apple-touch-icon" sizes="76x76" />
|
||||
<link href="/assets/apple-touch-icon-114x114.png" rel="apple-touch-icon" sizes="114x114" />
|
||||
<link href="/assets/apple-touch-icon-120x120.png" rel="apple-touch-icon" sizes="120x120" />
|
||||
<link href="/assets/apple-touch-icon-144x144.png" rel="apple-touch-icon" sizes="144x144" />
|
||||
<link href="/assets/apple-touch-icon-152x152.png" rel="apple-touch-icon" sizes="152x152" />
|
||||
<link href="/assets/apple-touch-icon-167x167.png" rel="apple-touch-icon" sizes="167x167" />
|
||||
<link href="/assets/apple-touch-icon-180x180.png" rel="apple-touch-icon" sizes="180x180" />
|
||||
<link href="/assets/apple-touch-icon-1024x1024.png" rel="apple-touch-icon" sizes="1024x1024" />
|
||||
<meta content="yes" name="apple-mobile-web-app-capable" />
|
||||
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style" />
|
||||
<meta content="StargazeTools" name="apple-mobile-web-app-title" />
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-640x1136.png"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1136x640.png"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-750x1334.png"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1334x750.png"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1125x2436.png"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2436x1125.png"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1170x2532.png"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2532x1170.png"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-828x1792.png"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1792x828.png"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1242x2688.png"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2688x1242.png"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1242x2208.png"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2208x1242.png"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1284x2778.png"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2778x1284.png"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1536x2048.png"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2048x1536.png"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1620x2160.png"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2160x1620.png"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1668x2388.png"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2388x1668.png"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-1668x2224.png"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2224x1668.png"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2048x2732.png"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-2732x2048.png"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
rel="apple-touch-startup-image"
|
||||
/>
|
||||
<meta content="#F0827D" name="msapplication-TileColor" />
|
||||
<meta content="/assets/mstile-144x144.png" name="msapplication-TileImage" />
|
||||
<meta content="/assets/browserconfig.xml" name="msapplication-config" />
|
||||
<link href="/assets/yandex-browser-manifest.json" rel="yandex-tableau-widget" />
|
||||
</>
|
||||
)
|
||||
}
|
30
components/FormControl.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps, ElementType, ReactNode } from 'react'
|
||||
|
||||
export interface FormControlProps extends ComponentProps<'div'> {
|
||||
title: string
|
||||
subtitle?: ReactNode
|
||||
htmlId?: string
|
||||
isRequired?: boolean
|
||||
labelAs?: ElementType
|
||||
}
|
||||
|
||||
export const FormControl = (props: FormControlProps) => {
|
||||
const { title, subtitle, htmlId, isRequired, children, className, labelAs: Label = 'label', ...rest } = props
|
||||
|
||||
return (
|
||||
<div className={clsx('flex flex-col space-y-2', className)} {...rest}>
|
||||
<Label className="flex flex-col space-y-1" htmlFor={htmlId}>
|
||||
<span
|
||||
className={clsx('font-bold first-letter:capitalize', {
|
||||
"after:text-red-500 after:content-['_*']": isRequired,
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
{subtitle && <span className="text-sm text-white/50">{subtitle}</span>}
|
||||
</Label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
23
components/FormGroup.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface FormGroupProps {
|
||||
title: string
|
||||
subtitle: ReactNode
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const FormGroup = (props: FormGroupProps) => {
|
||||
const { title, subtitle, children } = props
|
||||
|
||||
return (
|
||||
<div className="flex p-4 space-x-4 w-full">
|
||||
<div className="flex flex-col w-1/3">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="font-bold">{title}</span>
|
||||
{subtitle && <span className="text-sm text-white/50">{subtitle}</span>}
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-4 w-2/3">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
32
components/HomeCard.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
|
||||
export interface HomeCardProps extends ComponentProps<'div'> {
|
||||
title: string
|
||||
link?: string
|
||||
linkText?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const HomeCard = (props: HomeCardProps) => {
|
||||
const { title, link, linkText, children, className, ...rest } = props
|
||||
return (
|
||||
<div className={clsx('flex relative flex-col space-y-4', className)} {...rest}>
|
||||
<h2 className="font-heading text-xl font-bold">{title}</h2>
|
||||
<p className="flex-grow text-white/75">{children}</p>
|
||||
{link && (
|
||||
<Anchor
|
||||
className={clsx(
|
||||
'flex before:absolute before:inset-0 items-center space-x-1',
|
||||
'font-bold text-plumbus hover:underline',
|
||||
)}
|
||||
href={link}
|
||||
>
|
||||
<span>{linkText ?? 'Open Link'}</span> <FaArrowRight size={12} />
|
||||
</Anchor>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
4
components/Input.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { StyledInput } from './forms/StyledInput'
|
||||
|
||||
/** @deprecated - replace with {@link StyledInput} */
|
||||
export const Input = StyledInput
|
20
components/InputDateTime.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import clsx from 'clsx'
|
||||
import type { DateTimePickerProps } from 'react-datetime-picker/dist/entry.nostyle'
|
||||
import DateTimePicker from 'react-datetime-picker/dist/entry.nostyle'
|
||||
import { FaCalendar, FaTimes } from 'react-icons/fa'
|
||||
|
||||
export const InputDateTime = ({ className, ...rest }: DateTimePickerProps) => {
|
||||
return (
|
||||
<DateTimePicker
|
||||
calendarIcon={<FaCalendar className="text-white hover:text-white/80" />}
|
||||
className={clsx(
|
||||
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
className,
|
||||
)}
|
||||
clearIcon={<FaTimes className="text-plumbus-40 hover:text-plumbus-60" />}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
44
components/Issuebar.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { Button } from 'components/Button'
|
||||
import { toggleSidebar, useSidebarStore } from 'contexts/sidebar'
|
||||
import { FaChevronRight } from 'react-icons/fa'
|
||||
import { FiLink2 } from 'react-icons/fi'
|
||||
import { links } from 'utils/links'
|
||||
|
||||
export const Issuebar = () => {
|
||||
const { isOpen } = useSidebarStore()
|
||||
|
||||
return (
|
||||
<div className={clsx(isOpen ? 'min-w-[230px] max-w-[230px]' : 'min-w-[20px] max-w-[20px]', 'relative z-10')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-auto min-w-[230px] max-w-[230px] no-scrollbar',
|
||||
'bg-black/50 border-l-[1px] border-l-plumbus-light',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-4 p-6 pt-8 min-h-screen">
|
||||
<div>This is a beta version of StargazeTools.</div>
|
||||
<div>We are open for your feedback and suggestions!</div>
|
||||
<Anchor href={`${links.GitHub}/issues/new/choose`}>
|
||||
<Button rightIcon={<FiLink2 />} variant="outline">
|
||||
Submit an issue
|
||||
</Button>
|
||||
</Anchor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* sidebar toggle */}
|
||||
<button
|
||||
className={clsx(
|
||||
'absolute top-[32px] left-[-12px] p-1 w-[24px] h-[24px]',
|
||||
'text-black bg-plumbus-light rounded-full',
|
||||
'hover:bg-plumbus',
|
||||
)}
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<FaChevronRight className={clsx('mx-auto', { 'rotate-180': !isOpen })} size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
74
components/JsonPreview.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { useState } from 'react'
|
||||
import { FaChevronDown, FaRegClipboard, FaTimes } from 'react-icons/fa'
|
||||
import { copy } from 'utils/clipboard'
|
||||
|
||||
import { Tooltip } from './Tooltip'
|
||||
|
||||
export interface JsonPreviewProps {
|
||||
title?: string
|
||||
content: unknown
|
||||
|
||||
initialState?: boolean
|
||||
onClose?: () => void
|
||||
isCopyable?: boolean
|
||||
isToggleable?: boolean
|
||||
|
||||
/** @deprecated use {@link isCopyable} prop */
|
||||
copyable?: boolean
|
||||
/** @deprecated use {@link initialState} prop */
|
||||
isVisible?: boolean
|
||||
}
|
||||
|
||||
export const JsonPreview = ({
|
||||
copyable = false,
|
||||
isVisible = true,
|
||||
|
||||
title = 'JSON Preview',
|
||||
content,
|
||||
|
||||
initialState = isVisible,
|
||||
onClose,
|
||||
isCopyable = copyable,
|
||||
isToggleable = true,
|
||||
}: JsonPreviewProps) => {
|
||||
const [show, setShow] = useState(() => initialState)
|
||||
|
||||
const toggle = () => {
|
||||
if (isToggleable) setShow((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative bg-stone-800/80 rounded border-2 border-white/20 divide-y-2 divide-white/20">
|
||||
<div className="flex items-center py-1 px-2 space-x-2">
|
||||
<span className="text-sm font-bold">{title}</span>
|
||||
<div className="flex-grow" />
|
||||
{isToggleable && (
|
||||
<Tooltip label={show ? 'Hide content' : 'Show content'}>
|
||||
<button onClick={toggle} type="button">
|
||||
<FaChevronDown className={show ? 'rotate-180' : ''} size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isCopyable && (
|
||||
<Tooltip label="Copy to clipboard">
|
||||
<button onClick={() => void copy(JSON.stringify(content))} type="button">
|
||||
<FaRegClipboard size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onClose && (
|
||||
<Tooltip label="Close">
|
||||
<button className="text-plumbus" onClick={onClose} type="button">
|
||||
<FaTimes size={20} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{show && (
|
||||
<div className="overflow-auto p-2 font-mono text-sm">
|
||||
<pre>{JSON.stringify(content, null, 2).trim()}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
57
components/Layout.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import clsx from 'clsx'
|
||||
import Head from 'next/head'
|
||||
import type { ReactNode } from 'react'
|
||||
import { FaDesktop } from 'react-icons/fa'
|
||||
import type { PageMetadata } from 'utils/layout'
|
||||
|
||||
import { DefaultAppSeo } from './DefaultAppSeo'
|
||||
// import { Issuebar } from './Issuebar'
|
||||
import { Sidebar } from './Sidebar'
|
||||
|
||||
export interface LayoutProps {
|
||||
metadata?: PageMetadata
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const Layout = ({ children, metadata = {} }: LayoutProps) => {
|
||||
return (
|
||||
<div className="overflow-hidden relative">
|
||||
<Head>
|
||||
<meta content="minimum-scale=1, initial-scale=1, width=device-width" name="viewport" />
|
||||
</Head>
|
||||
|
||||
<DefaultAppSeo />
|
||||
|
||||
{/* plumbus confetti */}
|
||||
<div className="fixed inset-0 -z-10 pointer-events-none stargaze-gradient-bg opacity-50">
|
||||
<img alt="plumbus carina-nebula" className="fixed top-0 right-0 w-full h-[calc(100vh+180px)]" src="/carina-nebula.png" />
|
||||
</div>
|
||||
|
||||
{/* actual layout container */}
|
||||
<div className="hidden sm:flex">
|
||||
<Sidebar />
|
||||
<div className="overflow-auto relative flex-grow h-screen no-scrollbar">
|
||||
<main
|
||||
className={clsx('mx-auto max-w-7xl', {
|
||||
'flex flex-col justify-center items-center':
|
||||
typeof metadata.center === 'boolean' ? metadata.center : true,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
{/* <Issuebar /> */}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center items-center p-8 space-y-4 h-screen text-center bg-black/50 sm:hidden">
|
||||
<FaDesktop size={48} />
|
||||
<h1 className="text-2xl font-bold">Unsupported Viewport</h1>
|
||||
<p>
|
||||
StargazeTools is best viewed on the big screen.
|
||||
<br />
|
||||
Please open StargazeTools on your tablet or desktop browser.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
28
components/LinkTab.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
|
||||
export interface LinkTabProps {
|
||||
title: string
|
||||
description: string
|
||||
href: string
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export const LinkTab = (props: LinkTabProps) => {
|
||||
const { title, description, href, isActive } = props
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
className={clsx(
|
||||
'isolate p-4 space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
isActive ? 'border-plumbus' : 'border-transparent',
|
||||
isActive ? 'bg-plumbus/5 hover:bg-plumbus/10' : 'hover:bg-white/5',
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
<h4 className="font-bold">{title}</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">{description}</span>
|
||||
</Anchor>
|
||||
)
|
||||
}
|
19
components/LinkTabs.data.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { LinkTabProps } from './LinkTab'
|
||||
|
||||
export const cw721BaseLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Create a new CW721 Base contract`,
|
||||
href: '/contracts/cw721/base/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries with your CW721 Base contract`,
|
||||
href: '/contracts/cw721/base/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute CW721 Base contract actions`,
|
||||
href: '/contracts/cw721/base/execute',
|
||||
},
|
||||
]
|
24
components/LinkTabs.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type { LinkTabProps } from './LinkTab'
|
||||
import { LinkTab } from './LinkTab'
|
||||
|
||||
export interface LinkTabsProps {
|
||||
data: LinkTabProps[]
|
||||
activeIndex?: number
|
||||
}
|
||||
|
||||
export const LinkTabs = ({ data, activeIndex }: LinkTabsProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'grid before:absolute relative grid-flow-col items-stretch rounded',
|
||||
'before:inset-x-0 before:bottom-0 before:border-b-2 before:border-white/25',
|
||||
)}
|
||||
>
|
||||
{data.map((item, index) => (
|
||||
<LinkTab key={index} {...item} isActive={index === activeIndex} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
83
components/Modal.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import clsx from 'clsx'
|
||||
import { Button } from 'components/Button'
|
||||
import { Radio } from 'components/Radio'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FaAsterisk } from 'react-icons/fa'
|
||||
|
||||
export const Modal = () => {
|
||||
const [showModal, setShowModal] = useState(true)
|
||||
const [isButtonDisabled, setIsButtonDisabled] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('disclaimer')) {
|
||||
setShowModal(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const accept = () => {
|
||||
localStorage.setItem('disclaimer', '1')
|
||||
setShowModal(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal ? (
|
||||
<div className="flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 justify-center items-center outline-none focus:outline-none">
|
||||
<div className="relative my-6 mx-auto w-auto max-w-3xl">
|
||||
<div className="flex relative flex-col w-full bg-stone-800 rounded-lg border-[1px] border-slate-200/20 border-solid outline-none focus:outline-none shadow-lg">
|
||||
<div className="flex justify-between items-start p-5 rounded-t border-b border-slate-200/20 border-solid">
|
||||
<h3 className="text-3xl font-bold">Before using StargazeTools...</h3>
|
||||
</div>
|
||||
<div className="relative flex-auto p-6 my-4">
|
||||
<p className="text-lg leading-relaxed">
|
||||
StargazeTools is a decentralized application where
|
||||
individuals or communities can use smart contract dashboards
|
||||
to create, mint and manage NFT collections.
|
||||
<br />
|
||||
StargazeTools is made up of free, public, and open-source
|
||||
software that is built on top of Stargaze Network.
|
||||
StargazeTools only provides tools for any of the mentioned
|
||||
functionalities above and inside the dApp. Anyone can create
|
||||
or mint NFT collections on StargazeTools and StargazeTools
|
||||
does not audit or have any discretion on how these
|
||||
collections are put to use. <br />
|
||||
<br />
|
||||
AS DESCRIBED IN THE DISCLAIMER, STARGAZETOOLS IS PROVIDED
|
||||
“AS IS”, AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY
|
||||
KIND. No developer or entity involved in creating the
|
||||
StargazeTools will be liable for any claims or damages
|
||||
whatsoever associated with your use, inability to use, or
|
||||
your interaction with other users of the StargazeTools,
|
||||
including any direct, indirect, incidental, special,
|
||||
exemplary, punitive or consequential damages, or loss of
|
||||
profits, tokens, or anything else.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Radio
|
||||
checked={!isButtonDisabled}
|
||||
htmlFor="disclaimer-accept"
|
||||
id="disclaimer-accept"
|
||||
onChange={() => setIsButtonDisabled(false)}
|
||||
subtitle=""
|
||||
title="I've read the disclaimer and I understand the risks of using StargazeTools."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end items-center p-6 mt-1">
|
||||
<Button
|
||||
className={clsx({ 'opacity-50': isButtonDisabled })}
|
||||
disabled={isButtonDisabled}
|
||||
isWide
|
||||
leftIcon={<FaAsterisk />}
|
||||
onClick={accept}
|
||||
>
|
||||
Enter StargazeTools
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
15
components/PageHeader.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface PageHeaderProps {
|
||||
title: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const PageHeader = ({ title, children }: PageHeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-heading text-4xl font-bold">{title}</h1>
|
||||
<p>{children}</p>
|
||||
</>
|
||||
)
|
||||
}
|
41
components/Radio.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import type { ChangeEventHandler, ReactNode } from 'react'
|
||||
|
||||
export interface RadioProps<T = string> {
|
||||
id: string
|
||||
htmlFor: T
|
||||
title: string
|
||||
subtitle: string
|
||||
checked: boolean
|
||||
onChange: ChangeEventHandler<HTMLInputElement> | undefined
|
||||
selectSingle?: boolean
|
||||
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const Radio = (props: RadioProps) => {
|
||||
const { id, htmlFor, title, subtitle, checked, onChange, children, selectSingle = false } = props
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
{/* radio element */}
|
||||
<input
|
||||
checked={checked}
|
||||
className="mt-1 w-4 h-4 text-plumbus focus:ring-plumbus cursor-pointer form-radio"
|
||||
id={`${htmlFor}-${id}`}
|
||||
name={selectSingle ? id : htmlFor}
|
||||
onChange={onChange}
|
||||
type="radio"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col flex-grow space-y-2">
|
||||
{/* radio description */}
|
||||
<label className="group cursor-pointer" htmlFor={`${htmlFor}-${id}`}>
|
||||
<span className="block font-bold group-hover:underline">{title}</span>
|
||||
<span className="block text-sm whitespace-pre-wrap">{subtitle}</span>
|
||||
</label>
|
||||
|
||||
{/* children if checked */}
|
||||
{checked ? children : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
52
components/SearchInput.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { FaSearch } from 'react-icons/fa'
|
||||
|
||||
export interface SearchInputProps extends Omit<ComponentProps<'input'>, 'children'> {
|
||||
_container?: ComponentProps<'div'>
|
||||
value: string
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
export const SearchInput = (props: SearchInputProps) => {
|
||||
const { _container, value, onClear, ...rest } = props
|
||||
|
||||
return (
|
||||
<div className="relative" {..._container}>
|
||||
{/* search icon as label */}
|
||||
<label
|
||||
aria-label="Search"
|
||||
className="flex absolute inset-y-0 left-4 items-center text-white/50"
|
||||
htmlFor={props.id}
|
||||
>
|
||||
<FaSearch size={16} />
|
||||
</label>
|
||||
|
||||
{/* main search input element */}
|
||||
<input
|
||||
className={clsx(
|
||||
'py-2 pr-14 pl-10 w-[36ch] form-input placeholder-white/50',
|
||||
'bg-white/10 rounded border-2 border-white/25 focus:ring focus:ring-plumbus',
|
||||
)}
|
||||
placeholder="Search..."
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
{/* clear button, visible when search value exists */}
|
||||
{value.length > 0 && (
|
||||
<div className="flex absolute inset-y-0 right-2 items-center">
|
||||
<button
|
||||
className={clsx(
|
||||
'py-1 px-2 text-xs font-bold text-plumbus',
|
||||
'hover:bg-plumbus/10 rounded border border-plumbus',
|
||||
)}
|
||||
onClick={onClear}
|
||||
type="button"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
88
components/Sidebar.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useRouter } from 'next/router'
|
||||
import BrandText from 'public/brand/brand-text.svg'
|
||||
import { footerLinks, links, socialsLinks } from 'utils/links'
|
||||
|
||||
import { SidebarLayout } from './SidebarLayout'
|
||||
import { WalletLoader } from './WalletLoader'
|
||||
|
||||
const routes = [
|
||||
{ text: 'Create Collection', href: `/collection/create` },
|
||||
{ text: 'CW721 Base', href: `/contracts/cw721/base` },
|
||||
]
|
||||
|
||||
export const Sidebar = () => {
|
||||
const router = useRouter()
|
||||
const wallet = useWallet()
|
||||
|
||||
return (
|
||||
<SidebarLayout>
|
||||
{/* Stargaze brand as home button */}
|
||||
<Anchor href="/" onContextMenu={(e) => [e.preventDefault(), router.push('/brand')]}>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-1/2 h-16',
|
||||
'rounded border-2 border-white/20 border-dashed'
|
||||
)}
|
||||
>
|
||||
Home{/* <BrandText className="text-plumbus hover:text-plumbus-light transition" /> */}
|
||||
</div>
|
||||
</Anchor>
|
||||
|
||||
{/* wallet button */}
|
||||
<WalletLoader />
|
||||
|
||||
{/* main navigation routes */}
|
||||
{routes.map(({ text, href }) => (
|
||||
<Anchor
|
||||
key={href}
|
||||
className={clsx(
|
||||
'py-2 px-4 -mx-4 uppercase', // styling
|
||||
'hover:bg-white/5 transition-colors', // hover styling
|
||||
{ 'font-bold text-plumbus': router.asPath.startsWith(href) }, // active route styling
|
||||
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
{text}
|
||||
</Anchor>
|
||||
))}
|
||||
|
||||
<div className="flex-grow" />
|
||||
|
||||
{/* Stargaze network status */}
|
||||
<div className="text-sm">Network: {wallet.network}</div>
|
||||
|
||||
{/* footer reference links */}
|
||||
<ul className="text-sm list-disc list-inside">
|
||||
{footerLinks.map(({ href, text }) => (
|
||||
<li key={href}>
|
||||
<Anchor className="hover:text-plumbus hover:underline" href={href}>
|
||||
{text}
|
||||
</Anchor>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* footer attribution */}
|
||||
<div className="text-xs text-white/50">
|
||||
StargazeTools {process.env.APP_VERSION} <br />
|
||||
Made by{' '}
|
||||
<Anchor className="text-plumbus hover:underline" href={links.deuslabs}>
|
||||
deus labs
|
||||
</Anchor>
|
||||
</div>
|
||||
|
||||
{/* footer social links */}
|
||||
<div className="flex gap-x-6 items-center text-white/75">
|
||||
{socialsLinks.map(({ Icon, href, text }) => (
|
||||
<Anchor key={href} className="hover:text-plumbus" href={href}>
|
||||
<Icon aria-label={text} size={20} />
|
||||
</Anchor>
|
||||
))}
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
)
|
||||
}
|
47
components/SidebarLayout.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import clsx from 'clsx'
|
||||
import { toggleSidebar, useSidebarStore } from 'contexts/sidebar'
|
||||
import type { ReactNode } from 'react'
|
||||
import { FaChevronLeft } from 'react-icons/fa'
|
||||
|
||||
export interface SidebarLayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const SidebarLayout = ({ children }: SidebarLayoutProps) => {
|
||||
const { isOpen } = useSidebarStore()
|
||||
|
||||
return (
|
||||
<div className={clsx(isOpen ? 'min-w-[250px] max-w-[250px]' : 'min-w-[20px] max-w-[20px]', 'relative z-10')}>
|
||||
{/* fixed component */}
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-auto fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar',
|
||||
'bg-black/50 border-r-[1px] border-r-plumbus-light',
|
||||
{ 'translate-x-[-230px]': !isOpen },
|
||||
)}
|
||||
>
|
||||
{/* inner component */}
|
||||
<div
|
||||
className={clsx('flex flex-col gap-y-4 p-8 min-h-screen', {
|
||||
invisible: !isOpen,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* sidebar toggle */}
|
||||
<button
|
||||
className={clsx(
|
||||
'absolute top-[32px] right-[-12px] p-1 w-[24px] h-[24px]',
|
||||
'text-black bg-plumbus-light rounded-full',
|
||||
'hover:bg-plumbus',
|
||||
)}
|
||||
onClick={toggleSidebar}
|
||||
type="button"
|
||||
>
|
||||
<FaChevronLeft className={clsx('mx-auto', { 'rotate-180': !isOpen })} size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
32
components/StackedList.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
|
||||
export interface StackedListProps extends ComponentProps<'dl'> {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const StackedList = (props: StackedListProps) => {
|
||||
const { className, ...rest } = props
|
||||
|
||||
return (
|
||||
<dl
|
||||
className={clsx('bg-white/5 rounded border-2 border-white/25', 'divide-y-2 divide-white/25', className)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export interface StackedListItemProps extends ComponentProps<'dt'> {
|
||||
name: ReactNode
|
||||
}
|
||||
|
||||
StackedList.Item = function StackedListItem(props: StackedListItemProps) {
|
||||
const { name, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 py-3 px-4 hover:bg-white/5">
|
||||
<dd className="font-medium text-white/50">{name}</dd>
|
||||
<dt className={clsx('col-span-2', className)} {...rest} />
|
||||
</div>
|
||||
)
|
||||
}
|
25
components/Stats.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface StatsProps {
|
||||
title: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const Stats = ({ title, children }: StatsProps) => {
|
||||
return (
|
||||
<div className={clsx('flex flex-col p-6 space-y-1', 'bg-white/10 rounded border-2 border-white/20 backdrop-blur')}>
|
||||
<div className="font-bold text-white/50">{title}</div>
|
||||
<div className="text-4xl font-bold">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface StatsDenomProps {
|
||||
text: string | null
|
||||
}
|
||||
|
||||
Stats.Denom = function StatsDenom({ text }: StatsDenomProps) {
|
||||
/* Slice the first character because we get "u" for the denom */
|
||||
return <span className="font-mono text-xl">{text?.slice(1)}</span>
|
||||
}
|
17
components/TextArea.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
|
||||
export const TextArea = (props: ComponentProps<'textarea'>) => {
|
||||
const { className, ...rest } = props
|
||||
return (
|
||||
<textarea
|
||||
className={clsx(
|
||||
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
44
components/Tooltip.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps, ReactElement, ReactNode } from 'react'
|
||||
import { cloneElement, useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
|
||||
export interface TooltipProps extends ComponentProps<'div'> {
|
||||
label: ReactNode
|
||||
children: ReactElement
|
||||
}
|
||||
|
||||
export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||
const [referenceElement, setReferenceElement] = useState(null)
|
||||
const [popperElement, setPopperElement] = useState<any>(null)
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'top',
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* children with attached ref and mouse events */}
|
||||
{cloneElement(children, {
|
||||
...children.props,
|
||||
ref: setReferenceElement,
|
||||
onMouseOver: () => setShow(true),
|
||||
onMouseOut: () => setShow(false),
|
||||
})}
|
||||
|
||||
{/* popper element */}
|
||||
{show && (
|
||||
<div
|
||||
{...props}
|
||||
{...attributes.popper}
|
||||
className={clsx('py-1 px-2 m-1 text-sm bg-black/80 rounded shadow-md', props.className)}
|
||||
ref={setPopperElement}
|
||||
style={{ ...styles.popper, ...props.style }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
18
components/TooltipIcon.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import type { ReactNode } from 'react'
|
||||
import { FaRegQuestionCircle } from 'react-icons/fa'
|
||||
import type { IconBaseProps } from 'react-icons/lib'
|
||||
|
||||
interface TooltipIconProps extends IconBaseProps {
|
||||
label: ReactNode
|
||||
}
|
||||
|
||||
export const TooltipIcon = ({ label, ...rest }: TooltipIconProps) => {
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<span>
|
||||
<FaRegQuestionCircle className="cursor-help" {...rest} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
44
components/TransactionHash.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import clsx from 'clsx'
|
||||
import { NETWORK } from 'utils/constants'
|
||||
import { links } from 'utils/links'
|
||||
|
||||
import { AnchorButton } from './AnchorButton'
|
||||
import { StyledInput } from './forms/StyledInput'
|
||||
|
||||
interface TransactionHashProps {
|
||||
hash: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const TransactionHash = ({ hash, className }: TransactionHashProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
hash !== '' ? 'text-white/100' : 'text-white/50',
|
||||
'flex justify-end items-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<StyledInput
|
||||
className={clsx(
|
||||
'flex-auto w-fit',
|
||||
'bg-white/5 rounded border-0 border-white/20 focus:ring-0 form-input',
|
||||
hash !== '' ? 'text-white/100' : 'text-white/50',
|
||||
className,
|
||||
)}
|
||||
value={hash || 'Waiting for execution...'}
|
||||
/>
|
||||
<AnchorButton
|
||||
className={clsx('ml-2 text-white', hash === '' ? 'text-white/30 bg-opacity-20 hover:bg-opacity-10' : '')}
|
||||
href={`${links.Explorer}/tx${NETWORK === 'mainnet' ? 's' : ''}/${hash}`}
|
||||
onClick={(e) => {
|
||||
if (hash === '') e.preventDefault()
|
||||
}}
|
||||
>
|
||||
Go to Explorer
|
||||
</AnchorButton>
|
||||
</div>
|
||||
)
|
||||
}
|
32
components/WalletButton.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { forwardRef } from 'react'
|
||||
import { BiWallet } from 'react-icons/bi'
|
||||
import { FaSpinner } from 'react-icons/fa'
|
||||
|
||||
export interface WalletButtonProps extends ComponentProps<'button'> {
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export const WalletButton = forwardRef<HTMLButtonElement, WalletButtonProps>(function WalletButton(
|
||||
{ className, children, isLoading, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'flex gap-x-2 items-center text-sm font-bold uppercase truncate', // content styling
|
||||
'py-2 px-4 rounded border border-plumbus', // button styling
|
||||
'hover:bg-white/10 transition-colors', // hover styling
|
||||
{ 'cursor-wait': isLoading }, // loading styling
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
type="button"
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="animate-spin" size={16} /> : <BiWallet size={16} />}
|
||||
<span>{isLoading ? 'Loading...' : children}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
78
components/WalletLoader.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { useWallet, useWalletStore } from 'contexts/wallet'
|
||||
import { Fragment } from 'react'
|
||||
import { FaCopy, FaPowerOff, FaRedo } from 'react-icons/fa'
|
||||
import { copy } from 'utils/clipboard'
|
||||
import { convertDenomToReadable } from 'utils/convertDenomToReadable'
|
||||
import { getShortAddress } from 'utils/getShortAddress'
|
||||
|
||||
import { WalletButton } from './WalletButton'
|
||||
import { WalletPanelButton } from './WalletPanelButton'
|
||||
|
||||
export const WalletLoader = () => {
|
||||
const { address, balance, connect, disconnect, initializing: isLoading, initialized: isReady } = useWallet()
|
||||
|
||||
const displayName = useWalletStore((store) => store.name || getShortAddress(store.address))
|
||||
|
||||
return (
|
||||
<Popover className="my-8">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<div className="grid -mx-4">
|
||||
{!isReady && (
|
||||
<WalletButton className="w-full" isLoading={isLoading} onClick={() => void connect()}>
|
||||
Connect Wallet
|
||||
</WalletButton>
|
||||
)}
|
||||
|
||||
{isReady && (
|
||||
<Popover.Button as={WalletButton} className="w-full" isLoading={isLoading}>
|
||||
{displayName}
|
||||
</Popover.Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 -translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-1"
|
||||
>
|
||||
<Popover.Panel
|
||||
className={clsx(
|
||||
'absolute inset-x-4 mt-2',
|
||||
'bg-stone-800/80 rounded shadow-lg shadow-black/90 backdrop-blur-sm',
|
||||
'flex flex-col items-stretch text-sm divide-y divide-white/10',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center py-2 px-4 space-y-1 text-center">
|
||||
<span className="py-px px-2 mb-2 font-mono text-xs text-white/50 rounded-full border border-white/25">
|
||||
{getShortAddress(address)}
|
||||
</span>
|
||||
<div className="font-bold">Your Balances</div>
|
||||
{balance.map((val) => (
|
||||
<span key={`balance-${val.denom}`}>
|
||||
{convertDenomToReadable(val.amount)} {val.denom.slice(1, val.denom.length)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<WalletPanelButton Icon={FaCopy} onClick={() => void copy(address)}>
|
||||
Copy wallet address
|
||||
</WalletPanelButton>
|
||||
<WalletPanelButton Icon={FaRedo} onClick={() => void connect()}>
|
||||
Reconnect
|
||||
</WalletPanelButton>
|
||||
<WalletPanelButton Icon={FaPowerOff} onClick={() => [disconnect(), close()]}>
|
||||
Disconnect
|
||||
</WalletPanelButton>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
25
components/WalletPanelButton.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { forwardRef } from 'react'
|
||||
import type { IconType } from 'react-icons/lib'
|
||||
|
||||
export interface WalletPanelButtonProps extends ComponentProps<'button'> {
|
||||
Icon: IconType
|
||||
}
|
||||
|
||||
export const WalletPanelButton = forwardRef<HTMLButtonElement, WalletPanelButtonProps>(function WalletPanelButton(
|
||||
{ className, children, Icon, ...rest },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
className={clsx('flex items-center py-2 px-4 space-x-4 hover:bg-white/5', className)}
|
||||
ref={ref}
|
||||
type="button"
|
||||
{...rest}
|
||||
>
|
||||
<Icon />
|
||||
<span className="text-left">{children}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
7
components/contracts/cw721/base/ExecuteCombobox.hooks.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import type { ExecuteListItem } from 'utils/contracts/cw721/base/execute'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||
}
|
92
components/contracts/cw721/base/ExecuteCombobox.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
import type { ExecuteListItem } from 'utils/contracts/cw721/base/execute'
|
||||
import { EXECUTE_LIST } from 'utils/contracts/cw721/base/execute'
|
||||
|
||||
export interface ExecuteComboboxProps {
|
||||
value: ExecuteListItem | null
|
||||
onChange: (item: ExecuteListItem) => void
|
||||
}
|
||||
|
||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filtered =
|
||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="message-type"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Contract execute message type"
|
||||
title="Message Type"
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select message type"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Message type not found.
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
31
components/forms/AddressBalances.hooks.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import type { Balance } from './AddressBalances'
|
||||
|
||||
export function useAddressBalancesState() {
|
||||
const [record, setRecord] = useState<Record<string, Balance>>(() => ({
|
||||
[uid()]: { address: '', amount: '0' },
|
||||
}))
|
||||
|
||||
const entries = useMemo(() => Object.entries(record), [record])
|
||||
const values = useMemo(() => Object.values(record), [record])
|
||||
|
||||
function add(balance: Balance = { address: '', amount: '0' }) {
|
||||
setRecord((prev) => ({ ...prev, [uid()]: balance }))
|
||||
}
|
||||
|
||||
function update(key: string, balance = record[key]) {
|
||||
setRecord((prev) => ({ ...prev, [key]: balance }))
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
return setRecord((prev) => {
|
||||
const latest = { ...prev }
|
||||
delete latest[key]
|
||||
return latest
|
||||
})
|
||||
}
|
||||
|
||||
return { entries, values, add, update, remove }
|
||||
}
|
89
components/forms/AddressBalances.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import { useEffect, useId, useMemo } from 'react'
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||
|
||||
import { useInputState } from './FormInput.hooks'
|
||||
|
||||
export interface Balance {
|
||||
address: string
|
||||
amount: string
|
||||
}
|
||||
|
||||
export interface AddressBalancesProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
isRequired?: boolean
|
||||
entries: [string, Balance][]
|
||||
onAdd: () => void
|
||||
onChange: (key: string, balance: Balance) => void
|
||||
onRemove: (key: string) => void
|
||||
}
|
||||
|
||||
export function AddressBalances(props: AddressBalancesProps) {
|
||||
const { title, subtitle, isRequired, entries, onAdd, onChange, onRemove } = props
|
||||
return (
|
||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||
{entries.map(([id], i) => (
|
||||
<AddressBalance
|
||||
key={`ib-${id}`}
|
||||
id={id}
|
||||
isLast={i === entries.length - 1}
|
||||
onAdd={onAdd}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AddressBalanceProps {
|
||||
id: string
|
||||
isLast: boolean
|
||||
onAdd: AddressBalancesProps['onAdd']
|
||||
onChange: AddressBalancesProps['onChange']
|
||||
onRemove: AddressBalancesProps['onRemove']
|
||||
}
|
||||
|
||||
export function AddressBalance({ id, isLast, onAdd, onChange, onRemove }: AddressBalanceProps) {
|
||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||
|
||||
const htmlId = useId()
|
||||
|
||||
const addressState = useInputState({
|
||||
id: `ib-address-${htmlId}`,
|
||||
name: `ib-address-${htmlId}`,
|
||||
title: `Wallet Address`,
|
||||
})
|
||||
|
||||
const amountState = useInputState({
|
||||
id: `ib-balance-${htmlId}`,
|
||||
name: `ib-balance-${htmlId}`,
|
||||
title: `Balance`,
|
||||
placeholder: '0',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onChange(id, {
|
||||
address: addressState.value,
|
||||
amount: amountState.value,
|
||||
})
|
||||
}, [addressState.value, amountState.value, id])
|
||||
|
||||
return (
|
||||
<div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2">
|
||||
<AddressInput {...addressState} />
|
||||
<NumberInput {...amountState} />
|
||||
<div className="flex justify-end items-end pb-2 w-8">
|
||||
<button
|
||||
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full"
|
||||
onClick={() => (isLast ? onAdd() : onRemove(id))}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
31
components/forms/AddressList.hooks.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import type { Address } from './AddressList'
|
||||
|
||||
export function useAddressListState() {
|
||||
const [record, setRecord] = useState<Record<string, Address>>(() => ({
|
||||
[uid()]: { address: '' },
|
||||
}))
|
||||
|
||||
const entries = useMemo(() => Object.entries(record), [record])
|
||||
const values = useMemo(() => Object.values(record), [record])
|
||||
|
||||
function add(balance: Address = { address: '' }) {
|
||||
setRecord((prev) => ({ ...prev, [uid()]: balance }))
|
||||
}
|
||||
|
||||
function update(key: string, balance = record[key]) {
|
||||
setRecord((prev) => ({ ...prev, [key]: balance }))
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
return setRecord((prev) => {
|
||||
const latest = { ...prev }
|
||||
delete latest[key]
|
||||
return latest
|
||||
})
|
||||
}
|
||||
|
||||
return { entries, values, add, update, remove }
|
||||
}
|
79
components/forms/AddressList.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput } from 'components/forms/FormInput'
|
||||
import { useEffect, useId, useMemo } from 'react'
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||
|
||||
import { useInputState } from './FormInput.hooks'
|
||||
|
||||
export interface Address {
|
||||
address: string
|
||||
}
|
||||
|
||||
export interface AddressListProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
isRequired?: boolean
|
||||
entries: [string, Address][]
|
||||
onAdd: () => void
|
||||
onChange: (key: string, address: Address) => void
|
||||
onRemove: (key: string) => void
|
||||
}
|
||||
|
||||
export function AddressList(props: AddressListProps) {
|
||||
const { title, subtitle, isRequired, entries, onAdd, onChange, onRemove } = props
|
||||
return (
|
||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||
{entries.map(([id], i) => (
|
||||
<Address
|
||||
key={`ib-${id}`}
|
||||
id={id}
|
||||
isLast={i === entries.length - 1}
|
||||
onAdd={onAdd}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AddressProps {
|
||||
id: string
|
||||
isLast: boolean
|
||||
onAdd: AddressListProps['onAdd']
|
||||
onChange: AddressListProps['onChange']
|
||||
onRemove: AddressListProps['onRemove']
|
||||
}
|
||||
|
||||
export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) {
|
||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||
|
||||
const htmlId = useId()
|
||||
|
||||
const addressState = useInputState({
|
||||
id: `ib-address-${htmlId}`,
|
||||
name: `ib-address-${htmlId}`,
|
||||
title: ``,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onChange(id, {
|
||||
address: addressState.value,
|
||||
})
|
||||
}, [addressState.value, id])
|
||||
|
||||
return (
|
||||
<div className="grid relative grid-cols-[1fr_auto] space-x-2">
|
||||
<AddressInput {...addressState} />
|
||||
<div className="flex justify-end items-end pb-2 w-8">
|
||||
<button
|
||||
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full"
|
||||
onClick={() => (isLast ? onAdd() : onRemove(id))}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
48
components/forms/FormInput.hooks.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export interface UseInputStateProps {
|
||||
id: string
|
||||
name: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
defaultValue?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const useInputState = ({ defaultValue, ...args }: UseInputStateProps) => {
|
||||
const [value, setValue] = useState<string>(() => defaultValue ?? '')
|
||||
useEffect(() => {
|
||||
if (defaultValue) setValue(defaultValue)
|
||||
}, [defaultValue])
|
||||
return {
|
||||
value,
|
||||
onChange: (obj: string | ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setValue(typeof obj === 'string' ? obj : obj.target.value)
|
||||
},
|
||||
...args,
|
||||
}
|
||||
}
|
||||
|
||||
export interface UseNumberInputStateProps {
|
||||
id: string
|
||||
name: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
defaultValue?: number
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const useNumberInputState = (args: UseNumberInputStateProps) => {
|
||||
const [value, setValue] = useState<number>(() => args.defaultValue ?? 0)
|
||||
useEffect(() => {
|
||||
if (args.defaultValue) setValue(args.defaultValue)
|
||||
}, [args.defaultValue])
|
||||
return {
|
||||
value,
|
||||
onChange: (obj: number | ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(typeof obj === 'number' ? obj : obj.target.valueAsNumber)
|
||||
},
|
||||
...args,
|
||||
}
|
||||
}
|
77
components/forms/FormInput.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { StyledInput } from 'components/forms/StyledInput'
|
||||
import type { ComponentPropsWithRef } from 'react'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
interface BaseProps {
|
||||
id: string
|
||||
name: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
isRequired?: boolean
|
||||
}
|
||||
|
||||
type SlicedInputProps = Omit<ComponentPropsWithRef<'input'>, keyof BaseProps>
|
||||
|
||||
export type FormInputProps = BaseProps & SlicedInputProps
|
||||
|
||||
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function FormInput(props, ref) {
|
||||
const { id, name, title, subtitle, isRequired, ...rest } = props
|
||||
return (
|
||||
<FormControl htmlId={id} isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||
<StyledInput id={id} name={name} ref={ref} {...rest} />
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
||||
|
||||
export const AddressInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function AddressInput(props, ref) {
|
||||
return (
|
||||
<FormInput
|
||||
{...props}
|
||||
placeholder={props.placeholder || 'stars1234567890abcdefghijklmnopqrstuvwxyz...'}
|
||||
ref={ref}
|
||||
type="text"
|
||||
/>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
||||
|
||||
export const ValidatorAddressInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function ValidatorAddressInput(props, ref) {
|
||||
return (
|
||||
<FormInput
|
||||
{...props}
|
||||
placeholder={props.placeholder || 'starsvaloper1234567890abcdefghijklmnopqrstuvwxyz...'}
|
||||
ref={ref}
|
||||
type="text"
|
||||
/>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
||||
|
||||
export const NumberInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function NumberInput(props, ref) {
|
||||
return <FormInput {...props} ref={ref} type="number" />
|
||||
},
|
||||
//
|
||||
)
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function TextInput(props, ref) {
|
||||
return <FormInput {...props} ref={ref} type="text" />
|
||||
},
|
||||
//
|
||||
)
|
||||
|
||||
export const UrlInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function UrlInput(props, ref) {
|
||||
return <FormInput {...props} ref={ref} type="url" />
|
||||
},
|
||||
//
|
||||
)
|
54
components/forms/FormTextArea.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ComponentPropsWithRef } from 'react'
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
|
||||
import { JsonValidStatus } from './JsonValidStatus'
|
||||
import { StyledTextArea } from './StyledTextArea'
|
||||
|
||||
interface BaseProps {
|
||||
id: string
|
||||
name: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
type SlicedInputProps = Omit<ComponentPropsWithRef<'textarea'>, keyof BaseProps>
|
||||
|
||||
export type FormTextAreaProps = BaseProps & SlicedInputProps
|
||||
|
||||
export const FormTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
|
||||
function FormTextArea(props, ref) {
|
||||
const { id, name, title, subtitle, ...rest } = props
|
||||
|
||||
return (
|
||||
<FormControl htmlId={id} subtitle={subtitle} title={title}>
|
||||
<StyledTextArea id={id} name={name} ref={ref} {...rest} />
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
||||
|
||||
export const JsonTextArea = forwardRef<HTMLTextAreaElement, FormTextAreaProps>(
|
||||
function JsonTextArea(props, ref) {
|
||||
const { id, name, title, subtitle, className, ...rest } = props
|
||||
|
||||
const innerRef = useRef<HTMLTextAreaElement>(null)
|
||||
useImperativeHandle(ref, () => innerRef.current!)
|
||||
|
||||
return (
|
||||
<FormControl htmlId={id} subtitle={subtitle} title={title}>
|
||||
<StyledTextArea
|
||||
className={clsx('min-h-[8rem] font-mono text-sm', className)}
|
||||
id={id}
|
||||
name={name}
|
||||
ref={innerRef}
|
||||
{...rest}
|
||||
/>
|
||||
<JsonValidStatus textAreaRef={innerRef} />
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
47
components/forms/JsonValidStatus.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import clsx from 'clsx'
|
||||
import type { RefObject } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FaCheckCircle, FaTimesCircle } from 'react-icons/fa'
|
||||
import { parseJson } from 'utils/json'
|
||||
|
||||
export interface JsonValidStatusProps {
|
||||
textAreaRef: RefObject<HTMLTextAreaElement>
|
||||
}
|
||||
|
||||
export function JsonValidStatus({ textAreaRef }: JsonValidStatusProps) {
|
||||
const [valid, setValid] = useState<boolean | null>(null)
|
||||
const ValidIcon = valid ? FaCheckCircle : FaTimesCircle
|
||||
|
||||
useEffect(() => {
|
||||
const listen = () => {
|
||||
if (!textAreaRef.current) return
|
||||
try {
|
||||
if (textAreaRef.current.value.length > 0) {
|
||||
parseJson(textAreaRef.current.value)
|
||||
setValid(true)
|
||||
} else {
|
||||
setValid(null)
|
||||
}
|
||||
} catch {
|
||||
setValid(false)
|
||||
}
|
||||
}
|
||||
listen()
|
||||
textAreaRef.current?.addEventListener('input', listen)
|
||||
return () => textAreaRef.current?.removeEventListener('input', listen)
|
||||
})
|
||||
|
||||
if (valid === null) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('flex items-center space-x-1 text-sm', {
|
||||
'text-green-500': valid,
|
||||
'text-red-500': !valid,
|
||||
})}
|
||||
>
|
||||
<ValidIcon />
|
||||
<span>{valid ? 'valid json content' : 'invalid json content'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
21
components/forms/StyledInput.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const StyledInput = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
|
||||
function Input({ className, ...rest }, ref) {
|
||||
return (
|
||||
<input
|
||||
className={clsx(
|
||||
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
21
components/forms/StyledTextArea.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import clsx from 'clsx'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const StyledTextArea = forwardRef<HTMLTextAreaElement, ComponentProps<'textarea'>>(
|
||||
function Input({ className, ...rest }, ref) {
|
||||
return (
|
||||
<textarea
|
||||
className={clsx(
|
||||
'bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
27
config/app.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export interface MappedCoin {
|
||||
readonly denom: string
|
||||
readonly fractionalDigits: number
|
||||
}
|
||||
|
||||
export type CoinMap = Readonly<Record<string, MappedCoin>>
|
||||
|
||||
export interface FeeOptions {
|
||||
upload: number
|
||||
exec: number
|
||||
init: number
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
readonly chainId: string
|
||||
readonly chainName: string
|
||||
readonly addressPrefix: string
|
||||
readonly rpcUrl: string
|
||||
readonly httpUrl?: string
|
||||
readonly faucetUrl?: string
|
||||
readonly feeToken: string
|
||||
readonly stakingToken: string
|
||||
readonly coinMap: CoinMap
|
||||
readonly gasPrice: number
|
||||
readonly fees: FeeOptions
|
||||
readonly codeId?: number
|
||||
}
|
17
config/favicons.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"path": "/assets/",
|
||||
"appName": "StargazeTools",
|
||||
"appShortName": "StargazeTools",
|
||||
"appDescription": "Stargaze Tools is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collection in no time.",
|
||||
"developerName": "StargazeTools",
|
||||
"developerURL": "https://",
|
||||
"background": "#FFC27D",
|
||||
"theme_color": "#FFC27D",
|
||||
"icons": {
|
||||
"android": true,
|
||||
"appleIcon": true,
|
||||
"appleStartup": true,
|
||||
"favicons": true,
|
||||
"windows": true
|
||||
}
|
||||
}
|
3
config/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './app'
|
||||
export * from './keplr'
|
||||
export * from './network'
|
81
config/keplr.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { ChainInfo } from '@keplr-wallet/types'
|
||||
|
||||
import type { AppConfig } from './app'
|
||||
|
||||
export interface KeplrCoin {
|
||||
readonly coinDenom: string
|
||||
readonly coinMinimalDenom: string
|
||||
readonly coinDecimals: number
|
||||
}
|
||||
|
||||
export interface KeplrConfig {
|
||||
readonly chainId: string
|
||||
readonly chainName: string
|
||||
readonly rpc: string
|
||||
readonly rest?: string
|
||||
readonly bech32Config: {
|
||||
readonly bech32PrefixAccAddr: string
|
||||
readonly bech32PrefixAccPub: string
|
||||
readonly bech32PrefixValAddr: string
|
||||
readonly bech32PrefixValPub: string
|
||||
readonly bech32PrefixConsAddr: string
|
||||
readonly bech32PrefixConsPub: string
|
||||
}
|
||||
readonly currencies: readonly KeplrCoin[]
|
||||
readonly feeCurrencies: readonly KeplrCoin[]
|
||||
readonly stakeCurrency: KeplrCoin
|
||||
readonly gasPriceStep: {
|
||||
readonly low: number
|
||||
readonly average: number
|
||||
readonly high: number
|
||||
}
|
||||
readonly bip44: { readonly coinType: number }
|
||||
readonly coinType: number
|
||||
}
|
||||
|
||||
export const keplrConfig = (config: AppConfig): ChainInfo => ({
|
||||
chainId: config.chainId,
|
||||
chainName: config.chainName,
|
||||
rpc: config.rpcUrl,
|
||||
rest: config.httpUrl!,
|
||||
bech32Config: {
|
||||
bech32PrefixAccAddr: `${config.addressPrefix}`,
|
||||
bech32PrefixAccPub: `${config.addressPrefix}pub`,
|
||||
bech32PrefixValAddr: `${config.addressPrefix}valoper`,
|
||||
bech32PrefixValPub: `${config.addressPrefix}valoperpub`,
|
||||
bech32PrefixConsAddr: `${config.addressPrefix}valcons`,
|
||||
bech32PrefixConsPub: `${config.addressPrefix}valconspub`,
|
||||
},
|
||||
currencies: [
|
||||
{
|
||||
coinDenom: config.coinMap[config.feeToken].denom,
|
||||
coinMinimalDenom: config.feeToken,
|
||||
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||
},
|
||||
{
|
||||
coinDenom: config.coinMap[config.stakingToken].denom,
|
||||
coinMinimalDenom: config.stakingToken,
|
||||
coinDecimals: config.coinMap[config.stakingToken].fractionalDigits,
|
||||
},
|
||||
],
|
||||
feeCurrencies: [
|
||||
{
|
||||
coinDenom: config.coinMap[config.feeToken].denom,
|
||||
coinMinimalDenom: config.feeToken,
|
||||
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||
},
|
||||
],
|
||||
stakeCurrency: {
|
||||
coinDenom: config.coinMap[config.stakingToken].denom,
|
||||
coinMinimalDenom: config.stakingToken,
|
||||
coinDecimals: config.coinMap[config.stakingToken].fractionalDigits,
|
||||
},
|
||||
gasPriceStep: {
|
||||
low: config.gasPrice / 2,
|
||||
average: config.gasPrice,
|
||||
high: config.gasPrice * 2,
|
||||
},
|
||||
bip44: { coinType: 118 },
|
||||
coinType: 118,
|
||||
features: ['ibc-transfer', 'cosmwasm', 'ibc-go'],
|
||||
})
|
11
config/meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import faviconsJson from './favicons.json'
|
||||
|
||||
export const meta = {
|
||||
name: faviconsJson.appName,
|
||||
description: faviconsJson.appDescription,
|
||||
domain: 'stargaze.tools',
|
||||
url: faviconsJson.developerURL,
|
||||
twitter: {
|
||||
username: '@stargazetools',
|
||||
},
|
||||
}
|
42
config/network.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { AppConfig } from './app'
|
||||
|
||||
export const mainnetConfig: AppConfig = {
|
||||
chainId: 'stargaze-1',
|
||||
chainName: 'Stargaze',
|
||||
addressPrefix: 'stars',
|
||||
rpcUrl: 'https://rpc.stargaze-apis.com/',
|
||||
feeToken: 'ustars',
|
||||
stakingToken: 'ustars',
|
||||
coinMap: {
|
||||
ustars: { denom: 'STARS', fractionalDigits: 6 },
|
||||
},
|
||||
gasPrice: 0.025,
|
||||
fees: {
|
||||
upload: 1500000,
|
||||
init: 500000,
|
||||
exec: 200000,
|
||||
},
|
||||
}
|
||||
|
||||
export const testnetConfig: AppConfig = {
|
||||
chainId: 'elgafar-1',
|
||||
chainName: 'elgafar-1',
|
||||
addressPrefix: 'stars',
|
||||
rpcUrl: 'https://rpc.elgafar-1.stargaze-apis.com/',
|
||||
feeToken: 'ustars',
|
||||
stakingToken: 'ustars',
|
||||
coinMap: {
|
||||
ustars: { denom: 'STARS', fractionalDigits: 6 },
|
||||
},
|
||||
gasPrice: 0.025,
|
||||
fees: {
|
||||
upload: 1500000,
|
||||
init: 500000,
|
||||
exec: 200000,
|
||||
},
|
||||
}
|
||||
|
||||
export const getConfig = (network: string): AppConfig => {
|
||||
if (network === 'mainnet') return mainnetConfig
|
||||
return testnetConfig
|
||||
}
|
9
config/react-query.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { QueryClient } from 'react-query'
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
notifyOnChangeProps: 'tracked',
|
||||
},
|
||||
},
|
||||
})
|
60
contexts/contracts.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import type { UseCW721BaseContractProps } from 'contracts/cw721/base'
|
||||
import { useCW721BaseContract } from 'contracts/cw721/base'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import type { State } from 'zustand'
|
||||
import create from 'zustand'
|
||||
|
||||
/**
|
||||
* Contracts store type definitions
|
||||
*/
|
||||
export interface ContractsStore extends State {
|
||||
cw721Base: UseCW721BaseContractProps | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Contracts store default values as a separate variable for reusability
|
||||
*/
|
||||
export const defaultValues: ContractsStore = {
|
||||
cw721Base: null,
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint for contracts store using {@link defaultValues}
|
||||
*/
|
||||
export const useContracts = create<ContractsStore>(() => ({
|
||||
...defaultValues,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Contracts store provider to easily mount {@link ContractsSubscription}
|
||||
* to listen/subscribe to contract changes
|
||||
*/
|
||||
export const ContractsProvider = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<ContractsSubscription />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Contracts store subscriptions (side effects)
|
||||
*
|
||||
* TODO: refactor all contract logics to zustand store
|
||||
*/
|
||||
const ContractsSubscription = () => {
|
||||
const cw721Base = useCW721BaseContract()
|
||||
|
||||
useEffect(() => {
|
||||
useContracts.setState({
|
||||
cw721Base,
|
||||
})
|
||||
}, [
|
||||
cw721Base,
|
||||
//
|
||||
])
|
||||
|
||||
return null
|
||||
}
|
13
contexts/sidebar.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import create from 'zustand'
|
||||
|
||||
export const useSidebarStore = create(() => ({ isOpen: true }))
|
||||
|
||||
export const openSidebar = () => {
|
||||
useSidebarStore.setState({ isOpen: true })
|
||||
}
|
||||
export const closeSidebar = () => {
|
||||
useSidebarStore.setState({ isOpen: false })
|
||||
}
|
||||
export const toggleSidebar = () => {
|
||||
useSidebarStore.setState((prev) => ({ isOpen: !prev.isOpen }))
|
||||
}
|
289
contexts/wallet.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { Decimal } from '@cosmjs/math'
|
||||
import type { OfflineSigner } from '@cosmjs/proto-signing'
|
||||
import type { Coin } from '@cosmjs/stargate'
|
||||
import type { AppConfig } from 'config'
|
||||
import { getConfig, keplrConfig } from 'config'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { createTrackedSelector } from 'react-tracked'
|
||||
import { NETWORK } from 'utils/constants'
|
||||
import type { State } from 'zustand'
|
||||
import create from 'zustand'
|
||||
import { subscribeWithSelector } from 'zustand/middleware'
|
||||
|
||||
export interface KeplrWalletStore extends State {
|
||||
accountNumber: number
|
||||
address: string
|
||||
balance: Coin[]
|
||||
client: SigningCosmWasmClient | undefined
|
||||
config: AppConfig
|
||||
initialized: boolean
|
||||
initializing: boolean
|
||||
name: string
|
||||
network: string
|
||||
signer: OfflineSigner | undefined
|
||||
|
||||
readonly clear: () => void
|
||||
|
||||
readonly connect: (walletChange?: boolean | 'focus') => Promise<void>
|
||||
|
||||
readonly disconnect: () => void | Promise<void>
|
||||
|
||||
readonly getClient: () => SigningCosmWasmClient
|
||||
readonly getSigner: () => OfflineSigner
|
||||
|
||||
readonly init: (signer?: OfflineSigner) => void
|
||||
|
||||
readonly refreshBalance: (address?: string, balance?: Coin[]) => Promise<void>
|
||||
|
||||
readonly setNetwork: (network: string) => void
|
||||
|
||||
readonly updateSigner: (singer: OfflineSigner) => void
|
||||
|
||||
readonly setQueryClient: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility export for references still using `WalletContextType`
|
||||
*
|
||||
* @deprecated replace with {@link KeplrWalletStore}
|
||||
*/
|
||||
export type WalletContextType = KeplrWalletStore
|
||||
|
||||
/**
|
||||
* Keplr wallet store default values as a separate variable for reusability
|
||||
*/
|
||||
const defaultStates = {
|
||||
accountNumber: 0,
|
||||
address: '',
|
||||
balance: [],
|
||||
client: undefined,
|
||||
config: getConfig(NETWORK),
|
||||
initialized: false,
|
||||
initializing: true,
|
||||
name: '',
|
||||
network: NETWORK,
|
||||
signer: undefined,
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint for keplr wallet store using {@link defaultStates}
|
||||
*/
|
||||
export const useWalletStore = create(
|
||||
subscribeWithSelector<KeplrWalletStore>((set, get) => ({
|
||||
...defaultStates,
|
||||
clear: () => set({ ...defaultStates }),
|
||||
connect: async (walletChange = false) => {
|
||||
try {
|
||||
if (walletChange !== 'focus') set({ initializing: true })
|
||||
const { config, init } = get()
|
||||
const signer = await loadKeplrWallet(config)
|
||||
init(signer)
|
||||
if (walletChange) set({ initializing: false })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.message)
|
||||
set({ initializing: false })
|
||||
}
|
||||
},
|
||||
disconnect: () => {
|
||||
window.localStorage.clear()
|
||||
get().clear()
|
||||
set({ initializing: false })
|
||||
},
|
||||
getClient: () => get().client!,
|
||||
getSigner: () => get().signer!,
|
||||
init: (signer) => set({ signer }),
|
||||
refreshBalance: async (address = get().address, balance = get().balance) => {
|
||||
const { client, config } = get()
|
||||
if (!client) return
|
||||
balance.length = 0
|
||||
for (const denom in config.coinMap) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const coin = await client.getBalance(address, denom)
|
||||
if (coin) balance.push(coin)
|
||||
}
|
||||
set({ balance })
|
||||
},
|
||||
setNetwork: (network) => set({ network }),
|
||||
updateSigner: (signer) => set({ signer }),
|
||||
setQueryClient: async () => {
|
||||
try {
|
||||
const client = (await createQueryClient()) as SigningCosmWasmClient
|
||||
set({ client })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.message)
|
||||
set({ initializing: false })
|
||||
}
|
||||
},
|
||||
})),
|
||||
)
|
||||
|
||||
/**
|
||||
* Proxied keplr wallet store which only rerenders on called state values.
|
||||
*
|
||||
* Recommended if only consuming state; to set states, use {@link useWalletStore.setState}.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // this will rerender if any state values has changed
|
||||
* const { name } = useWalletStore()
|
||||
*
|
||||
* // this will rerender if only `name` has changed
|
||||
* const { name } = useWallet()
|
||||
* ```
|
||||
*/
|
||||
export const useWallet = createTrackedSelector<KeplrWalletStore>(useWalletStore)
|
||||
|
||||
/**
|
||||
* Keplr wallet store provider to easily mount {@link WalletSubscription}
|
||||
* to listen/subscribe various state changes.
|
||||
*
|
||||
*/
|
||||
export const WalletProvider = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<WalletSubscription />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Keplr wallet subscriptions (side effects)
|
||||
*/
|
||||
const WalletSubscription = () => {
|
||||
/**
|
||||
* Dispatch reconnecting wallet on first mount and register events to refresh
|
||||
* on keystore change and window refocus.
|
||||
*
|
||||
*/
|
||||
useEffect(() => {
|
||||
const walletAddress = window.localStorage.getItem('wallet_address')
|
||||
if (walletAddress) {
|
||||
void useWalletStore.getState().connect()
|
||||
} else {
|
||||
useWalletStore.setState({ initializing: false })
|
||||
useWalletStore.getState().setQueryClient()
|
||||
}
|
||||
|
||||
const listenChange = () => {
|
||||
void useWalletStore.getState().connect(true)
|
||||
}
|
||||
const listenFocus = () => {
|
||||
if (walletAddress) void useWalletStore.getState().connect('focus')
|
||||
}
|
||||
|
||||
window.addEventListener('keplr_keystorechange', listenChange)
|
||||
window.addEventListener('focus', listenFocus)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keplr_keystorechange', listenChange)
|
||||
window.removeEventListener('focus', listenFocus)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Watch signer changes to initialize client state.
|
||||
*
|
||||
*/
|
||||
useEffect(() => {
|
||||
return useWalletStore.subscribe(
|
||||
(x) => x.signer,
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
async (signer) => {
|
||||
try {
|
||||
if (!signer) {
|
||||
useWalletStore.setState({
|
||||
client: (await createQueryClient()) as SigningCosmWasmClient,
|
||||
})
|
||||
} else {
|
||||
useWalletStore.setState({
|
||||
client: await createClient({ signer }),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Watch client changes to refresh balance and sync wallet states.
|
||||
*
|
||||
*/
|
||||
useEffect(() => {
|
||||
return useWalletStore.subscribe(
|
||||
(x) => x.client,
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
async (client) => {
|
||||
const { config, refreshBalance, signer } = useWalletStore.getState()
|
||||
if (!signer || !client) return
|
||||
if (!window.keplr) {
|
||||
throw new Error('window.keplr not found')
|
||||
}
|
||||
const balance: Coin[] = []
|
||||
const address = (await signer.getAccounts())[0].address
|
||||
const account = await client.getAccount(address)
|
||||
const key = await window.keplr.getKey(config.chainId)
|
||||
await refreshBalance(address, balance)
|
||||
window.localStorage.setItem('wallet_address', address)
|
||||
useWalletStore.setState({
|
||||
accountNumber: account?.accountNumber || 0,
|
||||
address,
|
||||
balance,
|
||||
initialized: true,
|
||||
initializing: false,
|
||||
name: key.name || '',
|
||||
})
|
||||
},
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to create signing client based on {@link useWalletStore} resolved
|
||||
* config state.
|
||||
*
|
||||
* @param arg - Object argument requiring `signer`
|
||||
*/
|
||||
const createClient = ({ signer }: { signer: OfflineSigner }) => {
|
||||
const { config } = useWalletStore.getState()
|
||||
return SigningCosmWasmClient.connectWithSigner(config.rpcUrl, signer, {
|
||||
gasPrice: {
|
||||
amount: Decimal.fromUserInput('0.0025', 100),
|
||||
denom: config.feeToken,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createQueryClient = () => {
|
||||
const { config } = useWalletStore.getState()
|
||||
return SigningCosmWasmClient.connect(config.rpcUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to load keplr wallet signer.
|
||||
*
|
||||
* @param config - Application configuration
|
||||
*/
|
||||
const loadKeplrWallet = async (config: AppConfig) => {
|
||||
if (!window.getOfflineSigner || !window.keplr || !window.getOfflineSignerAuto) {
|
||||
throw new Error('Keplr extension is not available')
|
||||
}
|
||||
|
||||
await window.keplr.experimentalSuggestChain(keplrConfig(config))
|
||||
await window.keplr.enable(config.chainId)
|
||||
|
||||
const signer = await window.getOfflineSignerAuto(config.chainId)
|
||||
Object.assign(signer, {
|
||||
signAmino: (signer as any).signAmino ?? (signer as any).sign,
|
||||
})
|
||||
|
||||
return signer
|
||||
}
|
458
contracts/cw721/base/contract.ts
Normal file
@ -0,0 +1,458 @@
|
||||
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { toBase64, toUtf8 } from '@cosmjs/encoding'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
// import { isDeliverTxFailure } from '@cosmjs/stargate'
|
||||
// import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'
|
||||
// import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
|
||||
import { getExecuteFee } from 'utils/fees'
|
||||
|
||||
const jsonToBinary = (json: Record<string, unknown>): string => {
|
||||
return toBase64(toUtf8(JSON.stringify(json)))
|
||||
}
|
||||
|
||||
type Expiration = { at_height: number } | { at_time: string } | { never: object }
|
||||
|
||||
// interface ExecuteWithSignDataResponse {
|
||||
// signed: TxRaw
|
||||
// txHash: string
|
||||
// }
|
||||
|
||||
export interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
}
|
||||
|
||||
export interface CW721BaseInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
// Queries
|
||||
ownerOf: (tokenId: string, includeExpired?: boolean) => Promise<any>
|
||||
approval: (tokenId: string, spender: string, includeExpired?: boolean) => Promise<any>
|
||||
approvals: (tokenId: string, includeExpired?: boolean) => Promise<any>
|
||||
allOperators: (owner: string, includeExpired?: boolean, startAfter?: string, limit?: number) => Promise<any>
|
||||
numTokens: () => Promise<any>
|
||||
contractInfo: () => Promise<any>
|
||||
nftInfo: (tokenId: string) => Promise<any>
|
||||
allNftInfo: (tokenId: string, includeExpired?: boolean) => Promise<any>
|
||||
tokens: (owner: string, startAfter?: string, limit?: number) => Promise<any>
|
||||
allTokens: (startAfter?: string, limit?: number) => Promise<any>
|
||||
minter: () => Promise<any>
|
||||
|
||||
// Execute
|
||||
transferNft: (recipient: string, tokenId: string) => Promise<string>
|
||||
sendNft: (contract: string, tokenId: string, msg: Record<string, unknown>) => Promise<string>
|
||||
approve: (spender: string, tokenId: string, expires?: Expiration) => Promise<string>
|
||||
revoke: (spender: string, tokenId: string) => Promise<string>
|
||||
approveAll: (operator: string, expires?: Expiration) => Promise<string>
|
||||
revokeAll: (operator: string) => Promise<string>
|
||||
mint: (tokenId: string, owner: string, tokenUri?: string) => Promise<string>
|
||||
burn: (tokenId: string) => Promise<string>
|
||||
}
|
||||
|
||||
export interface CW721BaseMessages {
|
||||
transferNft: (contractAddress: string, recipient: string, tokenId: string) => TransferNftMessage
|
||||
sendNft: (contractAddress: string, contract: string, tokenId: string, msg: Record<string, unknown>) => SendNFtMessage
|
||||
approve: (contractAddress: string, spender: string, tokenId: string, expires?: Expiration) => ApproveMessage
|
||||
revoke: (contractAddress: string, spender: string, tokenId: string) => RevokeMessage
|
||||
approveAll: (contractAddress: string, operator: string, expires?: Expiration) => ApproveAllMessage
|
||||
revokeAll: (contractAddress: string, operator: string) => RevokeAllMessage
|
||||
mint: (contractAddress: string, tokenId: string, owner: string, tokenUri?: string) => MintMessage
|
||||
burn: (contractAddress: string, tokenId: string) => BurnMessage
|
||||
}
|
||||
|
||||
export interface TransferNftMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
transfer_nft: {
|
||||
recipient: string
|
||||
token_id: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface SendNFtMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
send_nft: {
|
||||
contract: string
|
||||
token_id: string
|
||||
msg: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface ApproveMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
approve: {
|
||||
spender: string
|
||||
token_id: string
|
||||
expires?: Expiration
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface RevokeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
revoke: {
|
||||
spender: string
|
||||
token_id: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface ApproveAllMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
approve_all: {
|
||||
operator: string
|
||||
expires?: Expiration
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface RevokeAllMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
revoke_all: {
|
||||
operator: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint: {
|
||||
token_id: string
|
||||
owner: string
|
||||
token_uri?: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface BurnMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
burn: {
|
||||
token_id: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface CW721BaseContract {
|
||||
instantiate: (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
) => Promise<InstantiateResponse>
|
||||
|
||||
use: (contractAddress: string) => CW721BaseInstance
|
||||
|
||||
messages: () => CW721BaseMessages
|
||||
}
|
||||
|
||||
export const CW721Base = (client: SigningCosmWasmClient, txSigner: string): CW721BaseContract => {
|
||||
const fee = getExecuteFee()
|
||||
|
||||
const use = (contractAddress: string): CW721BaseInstance => {
|
||||
const ownerOf = async (tokenId: string, includeExpired?: boolean) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
owner_of: { token_id: tokenId, include_expired: includeExpired },
|
||||
})
|
||||
}
|
||||
|
||||
const approval = async (tokenId: string, spender: string, includeExpired?: boolean) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
approval: { token_id: tokenId, spender, include_expired: includeExpired },
|
||||
})
|
||||
}
|
||||
|
||||
const approvals = async (tokenId: string, includeExpired?: boolean) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
approvals: { token_id: tokenId, include_expired: includeExpired },
|
||||
})
|
||||
}
|
||||
|
||||
const allOperators = async (owner: string, includeExpired?: boolean, startAfter?: string, limit?: number) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
all_operators: { owner, include_expired: includeExpired, start_after: startAfter, limit },
|
||||
})
|
||||
}
|
||||
|
||||
const numTokens = async () => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
num_tokens: {},
|
||||
})
|
||||
}
|
||||
|
||||
const contractInfo = async () => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
contract_info: {},
|
||||
})
|
||||
}
|
||||
|
||||
const nftInfo = async (tokenId: string) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
nft_info: { token_id: tokenId },
|
||||
})
|
||||
}
|
||||
|
||||
const allNftInfo = async (tokenId: string, includeExpired?: boolean) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
all_nft_info: { token_id: tokenId, include_expired: includeExpired },
|
||||
})
|
||||
}
|
||||
|
||||
const tokens = async (owner: string, startAfter?: string, limit?: number) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
tokens: { owner, start_after: startAfter, limit },
|
||||
})
|
||||
}
|
||||
|
||||
const allTokens = async (startAfter?: string, limit?: number) => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
all_tokens: { start_after: startAfter, limit },
|
||||
})
|
||||
}
|
||||
|
||||
const minter = async () => {
|
||||
return client.queryContractSmart(contractAddress, {
|
||||
minter: {},
|
||||
})
|
||||
}
|
||||
|
||||
const transferNft = async (recipient: string, tokenId: string): Promise<string> => {
|
||||
const result = await client.execute(
|
||||
txSigner,
|
||||
contractAddress,
|
||||
{ transfer_nft: { recipient, token_id: tokenId } },
|
||||
fee,
|
||||
)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
const sendNft = async (contract: string, tokenId: string, msg: Record<string, unknown>): Promise<string> => {
|
||||
const result = await client.execute(
|
||||
txSigner,
|
||||
contractAddress,
|
||||
{ send_nft: { contract, token_id: tokenId, msg: jsonToBinary(msg) } },
|
||||
fee,
|
||||
)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
const approve = async (spender: string, tokenId: string, expires?: Expiration): Promise<string> => {
|
||||
const result = await client.execute(
|
||||
txSigner,
|
||||
contractAddress,
|
||||
{ approve: { spender, token_id: tokenId, expires } },
|
||||
fee,
|
||||
)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
const revoke = async (spender: string, tokenId: string): Promise<string> => {
|
||||
const result = await client.execute(txSigner, contractAddress, { revoke: { spender, token_id: tokenId } }, fee)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
const approveAll = async (operator: string, expires?: Expiration): Promise<string> => {
|
||||
const result = await client.execute(txSigner, contractAddress, { approve_all: { operator, expires } }, fee)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
const revokeAll = async (operator: string): Promise<string> => {
|
||||
const result = await client.execute(txSigner, contractAddress, { revoke_all: { operator } }, fee)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
const mint = async (tokenId: string, owner: string, tokenUri?: string): Promise<string> => {
|
||||
const result = await client.execute(
|
||||
txSigner,
|
||||
contractAddress,
|
||||
{ mint: { token_id: tokenId, owner, token_uri: tokenUri } },
|
||||
fee,
|
||||
)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
const burn = async (tokenId: string): Promise<string> => {
|
||||
const result = await client.execute(txSigner, contractAddress, { burn: { token_id: tokenId } }, fee)
|
||||
return result.transactionHash
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
ownerOf,
|
||||
approval,
|
||||
approvals,
|
||||
allOperators,
|
||||
numTokens,
|
||||
contractInfo,
|
||||
nftInfo,
|
||||
allNftInfo,
|
||||
tokens,
|
||||
allTokens,
|
||||
minter,
|
||||
transferNft,
|
||||
sendNft,
|
||||
approve,
|
||||
revoke,
|
||||
approveAll,
|
||||
revokeAll,
|
||||
mint,
|
||||
burn,
|
||||
}
|
||||
}
|
||||
|
||||
const instantiate = async (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
): Promise<InstantiateResponse> => {
|
||||
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
|
||||
memo: '',
|
||||
admin,
|
||||
})
|
||||
|
||||
return {
|
||||
contractAddress: result.contractAddress,
|
||||
transactionHash: result.transactionHash,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = () => {
|
||||
const transferNft = (contractAddress: string, recipient: string, tokenId: string): TransferNftMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
transfer_nft: { recipient, token_id: tokenId },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const sendNft = (
|
||||
contractAddress: string,
|
||||
contract: string,
|
||||
tokenId: string,
|
||||
msg: Record<string, unknown>,
|
||||
): SendNFtMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
send_nft: { contract, token_id: tokenId, msg },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const approve = (
|
||||
contractAddress: string,
|
||||
spender: string,
|
||||
tokenId: string,
|
||||
expires?: Expiration,
|
||||
): ApproveMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
approve: { spender, token_id: tokenId, expires },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const revoke = (contractAddress: string, spender: string, tokenId: string): RevokeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
revoke: { spender, token_id: tokenId },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const approveAll = (contractAddress: string, operator: string, expires?: Expiration): ApproveAllMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
approve_all: { operator, expires },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const revokeAll = (contractAddress: string, operator: string): RevokeAllMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
revoke_all: { operator },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const mint = (contractAddress: string, tokenId: string, owner: string, tokenUri?: string): MintMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint: { token_id: tokenId, owner, token_uri: tokenUri },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const burn = (contractAddress: string, tokenId: string): BurnMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
burn: { token_id: tokenId },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transferNft,
|
||||
sendNft,
|
||||
approve,
|
||||
revoke,
|
||||
approveAll,
|
||||
revokeAll,
|
||||
mint,
|
||||
burn,
|
||||
}
|
||||
}
|
||||
|
||||
return { instantiate, use, messages }
|
||||
}
|
2
contracts/cw721/base/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './contract'
|
||||
export * from './useContract'
|
73
contracts/cw721/base/useContract.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { CW721BaseContract, CW721BaseInstance, CW721BaseMessages } from './contract'
|
||||
import { CW721Base as initContract } from './contract'
|
||||
|
||||
interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
}
|
||||
|
||||
export interface UseCW721BaseContractProps {
|
||||
instantiate: (
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
) => Promise<InstantiateResponse>
|
||||
use: (customAddress: string) => CW721BaseInstance | undefined
|
||||
updateContractAddress: (contractAddress: string) => void
|
||||
messages: () => CW721BaseMessages | undefined
|
||||
}
|
||||
|
||||
export function useCW721BaseContract(): UseCW721BaseContractProps {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [address, setAddress] = useState<string>('')
|
||||
const [CW721Base, setCW721Base] = useState<CW721BaseContract>()
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(localStorage.getItem('contract_address') || '')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const cw721BaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setCW721Base(cw721BaseContract)
|
||||
}, [wallet])
|
||||
|
||||
const updateContractAddress = (contractAddress: string) => {
|
||||
setAddress(contractAddress)
|
||||
}
|
||||
|
||||
const instantiate = useCallback(
|
||||
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!CW721Base) {
|
||||
reject(new Error('Contract is not initialized.'))
|
||||
return
|
||||
}
|
||||
CW721Base.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
|
||||
})
|
||||
},
|
||||
[CW721Base, wallet],
|
||||
)
|
||||
|
||||
const use = useCallback(
|
||||
(customAddress = ''): CW721BaseInstance | undefined => {
|
||||
return CW721Base?.use(address || customAddress)
|
||||
},
|
||||
[CW721Base, address],
|
||||
)
|
||||
|
||||
const messages = useCallback((): CW721BaseMessages | undefined => {
|
||||
return CW721Base?.messages()
|
||||
}, [CW721Base])
|
||||
|
||||
return {
|
||||
instantiate,
|
||||
use,
|
||||
updateContractAddress,
|
||||
messages,
|
||||
}
|
||||
}
|
41
env.d.ts
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
/* eslint-disable import/no-default-export */
|
||||
|
||||
declare module '*.svg' {
|
||||
const Component: (props: import('react').SVGProps<SVGSVGElement>) => JSX.Element
|
||||
export default Component
|
||||
}
|
||||
|
||||
declare module 'react-datetime-picker/dist/entry.nostyle' {
|
||||
export { default } from 'react-datetime-picker'
|
||||
export * from 'react-datetime-picker'
|
||||
}
|
||||
|
||||
declare namespace NodeJS {
|
||||
declare interface ProcessEnv {
|
||||
readonly APP_VERSION: string
|
||||
|
||||
readonly NEXT_PUBLIC_CW721_BASE_CODE_ID: string
|
||||
|
||||
readonly NEXT_PUBLIC_API_URL: string
|
||||
readonly NEXT_PUBLIC_BLOCK_EXPLORER_URL: string
|
||||
readonly NEXT_PUBLIC_NETWORK: string
|
||||
readonly NEXT_PUBLIC_WEBSITE_URL: string
|
||||
|
||||
readonly NEXT_PUBLIC_S3_BUCKET: string
|
||||
readonly NEXT_PUBLIC_S3_ENDPOINT: string
|
||||
readonly NEXT_PUBLIC_S3_KEY: string
|
||||
readonly NEXT_PUBLIC_S3_REGION: string
|
||||
readonly NEXT_PUBLIC_S3_SECRET: string
|
||||
}
|
||||
}
|
||||
|
||||
type KeplrWindow = import('@keplr-wallet/types/src/window').Window
|
||||
|
||||
declare interface Window extends KeplrWindow {
|
||||
confetti?: (obj: any) => void
|
||||
}
|
||||
|
||||
declare const __DEV__: boolean
|
||||
declare const __PROD__: boolean
|
||||
|
||||
/* eslint-enable import/no-default-export */
|
19
hooks/useInterval.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const useInterval = (callback: () => void, delay: number) => {
|
||||
const savedCallback = useRef(callback)
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback
|
||||
}, [callback])
|
||||
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current()
|
||||
}
|
||||
if (delay !== null) {
|
||||
const id = setInterval(tick, delay)
|
||||
return () => clearInterval(id)
|
||||
}
|
||||
}, [delay])
|
||||
}
|
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
37
next.config.js
Normal file
@ -0,0 +1,37 @@
|
||||
// @ts-check
|
||||
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const LOCALHOST_URL = `http://localhost:${process.env.PORT || 3000}`
|
||||
|
||||
/**
|
||||
* @type {import("next").NextConfig}
|
||||
* @see https://nextjs.org/docs/api-reference/next.config.js/introduction
|
||||
*/
|
||||
const nextConfig = {
|
||||
env: {
|
||||
APP_VERSION: packageJson.version,
|
||||
NEXT_PUBLIC_WEBSITE_URL:
|
||||
process.env.NODE_ENV === 'development' ? LOCALHOST_URL : process.env.NEXT_PUBLIC_WEBSITE_URL,
|
||||
},
|
||||
reactStrictMode: true,
|
||||
trailingSlash: true,
|
||||
webpack(config, { dev, webpack }) {
|
||||
// svgr integration
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/i,
|
||||
issuer: /\.[jt]sx?$/,
|
||||
use: ['@svgr/webpack'],
|
||||
})
|
||||
// predefined constants
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: dev,
|
||||
__PROD__: !dev,
|
||||
}),
|
||||
)
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
80
package.json
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "stargaze-tools",
|
||||
"version": "0.1.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "eslint --fix \"**/*.{js,cjs,mjs,jsx,ts,tsx}\" && tsc --noEmit --pretty",
|
||||
"prepare": "husky install",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3",
|
||||
"@cosmjs/cosmwasm-stargate": "^0",
|
||||
"@cosmjs/encoding": "^0",
|
||||
"@cosmjs/math": "^0",
|
||||
"@cosmjs/proto-signing": "^0",
|
||||
"@cosmjs/stargate": "^0",
|
||||
"@fontsource/jetbrains-mono": "^4",
|
||||
"@fontsource/roboto": "^4",
|
||||
"@headlessui/react": "^1",
|
||||
"@keplr-wallet/cosmos": "^0.9.16",
|
||||
"@popperjs/core": "^2",
|
||||
"@svgr/webpack": "^6",
|
||||
"@tailwindcss/forms": "^0",
|
||||
"@tailwindcss/line-clamp": "^0",
|
||||
"axios": "^0",
|
||||
"clsx": "^1",
|
||||
"compare-versions": "^4",
|
||||
"match-sorter": "^6",
|
||||
"next": "^12",
|
||||
"next-seo": "^4",
|
||||
"react": "^18",
|
||||
"react-datetime-picker": "^3",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7",
|
||||
"react-hot-toast": "^2",
|
||||
"react-icons": "^4",
|
||||
"react-popper": "^2",
|
||||
"react-query": "^3",
|
||||
"react-tracked": "^1",
|
||||
"scheduler": "^0",
|
||||
"zustand": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14",
|
||||
"@types/react": "^18",
|
||||
"@types/react-datetime-picker": "^3",
|
||||
"autoprefixer": "^10",
|
||||
"husky": "^7",
|
||||
"lint-staged": "^12",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3",
|
||||
"typescript": "^4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@stargaze-tools/eslint-config"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
".next",
|
||||
"node_modules",
|
||||
"out",
|
||||
"public"
|
||||
],
|
||||
"root": true
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"**/*.{js,cjs,mjs,jsx,ts,tsx}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"prettier": "@stargaze-tools/prettier-config",
|
||||
"private": true
|
||||
}
|
71
packages/eslint-config/index.js
Normal file
@ -0,0 +1,71 @@
|
||||
// @ts-check
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const tsConfig = fs.existsSync('tsconfig.json')
|
||||
? path.resolve('tsconfig.json')
|
||||
: fs.existsSync('types/tsconfig.json')
|
||||
? path.resolve('types/tsconfig.json')
|
||||
: undefined
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
const eslintConfig = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
require.resolve('@vercel/style-guide/eslint/_base'),
|
||||
require.resolve('@vercel/style-guide/eslint/react'),
|
||||
require.resolve('@vercel/style-guide/eslint/next'),
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
plugins: ['simple-import-sort', 'tailwindcss', 'unused-imports'],
|
||||
rules: {
|
||||
'@next/next/no-img-element': ['off'],
|
||||
'import/extensions': ['off'],
|
||||
'import/newline-after-import': ['warn'],
|
||||
'import/order': ['off'],
|
||||
'no-console': ['warn'],
|
||||
'no-unused-vars': ['off'],
|
||||
'react/function-component-definition': ['off'],
|
||||
'react/jsx-sort-props': ['warn', { reservedFirst: ['key'] }],
|
||||
'simple-import-sort/exports': ['warn'],
|
||||
'simple-import-sort/imports': ['warn'],
|
||||
'sort-imports': ['off'],
|
||||
'tailwindcss/classnames-order': ['warn'],
|
||||
'unicorn/filename-case': ['error', { cases: { camelCase: true, kebabCase: true, pascalCase: true } }],
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.d.ts', '**/*.ts', '**/*.tsx'],
|
||||
extends: [require.resolve('@vercel/style-guide/eslint/typescript'), 'plugin:prettier/recommended'],
|
||||
parserOptions: {
|
||||
project: tsConfig,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-misused-promises': ['warn'],
|
||||
'@typescript-eslint/no-unsafe-argument': ['warn'],
|
||||
'@typescript-eslint/no-unsafe-assignment': ['warn'],
|
||||
'@typescript-eslint/no-unsafe-member-access': ['warn'],
|
||||
'@typescript-eslint/no-unsafe-return': ['warn'],
|
||||
'@typescript-eslint/no-unused-vars': ['off'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['pages/**/*.{js,jsx,ts,tsx}', 'next.config.{js,cjs,mjs}'],
|
||||
rules: {
|
||||
'import/no-default-export': ['off'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = eslintConfig
|
31
packages/eslint-config/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@stargaze-tools/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7",
|
||||
"@vercel/style-guide": "^3",
|
||||
"eslint": "^8",
|
||||
"eslint-plugin-prettier": "^4",
|
||||
"eslint-plugin-simple-import-sort": "^7",
|
||||
"eslint-plugin-tailwindcss": "^3",
|
||||
"eslint-plugin-unused-imports": "^2",
|
||||
"prettier": "^2",
|
||||
"typescript": "^4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"./index.js"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"root": true
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"prettier": "@stargaze-tools/prettier-config",
|
||||
"license": "MIT"
|
||||
}
|
12
packages/prettier-config/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import("prettier").Config} */
|
||||
const prettierConfig = {
|
||||
endOfLine: 'auto',
|
||||
printWidth: 120,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
}
|
||||
|
||||
module.exports = prettierConfig
|
26
packages/prettier-config/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@stargaze-tools/prettier-config",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"prettier": "^2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/prettier": "^2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@stargaze-tools/eslint-config"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"node_modules"
|
||||
],
|
||||
"root": true
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"prettier": "./index.js",
|
||||
"license": "MIT"
|
||||
}
|
4
pages/404.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
// pages/404.js
|
||||
export default function Custom404() {
|
||||
return <h1>Page Not Found</h1>
|
||||
}
|
30
pages/_app.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import '@fontsource/jetbrains-mono/latin.css'
|
||||
import '@fontsource/roboto/latin.css'
|
||||
import '../styles/globals.css'
|
||||
import '../styles/datepicker.css'
|
||||
|
||||
import { Layout } from 'components/Layout'
|
||||
import { Modal } from 'components/Modal'
|
||||
import { queryClient } from 'config/react-query'
|
||||
import { ContractsProvider } from 'contexts/contracts'
|
||||
import { WalletProvider } from 'contexts/wallet'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { QueryClientProvider } from 'react-query'
|
||||
import { getComponentMetadata } from 'utils/layout'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WalletProvider>
|
||||
<ContractsProvider>
|
||||
<Toaster position="top-right" />
|
||||
<Layout metadata={getComponentMetadata(Component)}>
|
||||
<Component {...pageProps} />
|
||||
<Modal />
|
||||
</Layout>
|
||||
</ContractsProvider>
|
||||
</WalletProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
22
pages/_document.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { FaviconsMetaTags } from 'components/FaviconsMetaTags'
|
||||
import NextDocument, { Head, Html, Main, NextScript } from 'next/document'
|
||||
import * as React from 'react'
|
||||
|
||||
export default class CustomDocument extends NextDocument {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta content="ie=edge" httpEquiv="X-UA-Compatible" />
|
||||
<FaviconsMetaTags />
|
||||
</Head>
|
||||
|
||||
<body className="font-sans antialiased">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
48
pages/brand.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import type { BrandPreviewProps } from 'components/BrandPreview'
|
||||
import { BrandPreview } from 'components/BrandPreview'
|
||||
import type { NextPage } from 'next'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
|
||||
const ASSETS: BrandPreviewProps[] = [
|
||||
{
|
||||
name: 'StargazeTools',
|
||||
id: 'brand',
|
||||
url: 'brand/brand.svg',
|
||||
Asset: dynamic(() => import('public/brand/brand.svg')),
|
||||
},
|
||||
{
|
||||
name: 'StargazeTools Bust',
|
||||
id: 'brand-bust',
|
||||
url: 'brand/brand-bust.svg',
|
||||
Asset: dynamic(() => import('public/brand/brand-bust.svg')),
|
||||
},
|
||||
{
|
||||
name: 'StargazeTools Text',
|
||||
id: 'brand-text',
|
||||
url: 'brand/brand-text.svg',
|
||||
Asset: dynamic(() => import('public/brand/brand-text.svg')),
|
||||
},
|
||||
]
|
||||
|
||||
const BrandPage: NextPage = () => {
|
||||
return (
|
||||
<section className="p-8 pb-16 space-y-8">
|
||||
<NextSeo title="Brand Assets" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold">Brand Assets</h1>
|
||||
<p>View and download StargazeTools brand assets</p>
|
||||
</div>
|
||||
|
||||
<hr className="border-white/20" />
|
||||
|
||||
{ASSETS.map((props, i) => (
|
||||
<BrandPreview key={`asset-${i}`} {...props} />
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(BrandPage, { center: false })
|
148
pages/contracts/cw721/base/execute.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { Button } from 'components/Button'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { ExecuteCombobox } from 'components/contracts/cw721/base/ExecuteCombobox'
|
||||
import { useExecuteComboboxState } from 'components/contracts/cw721/base/ExecuteCombobox.hooks'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonTextArea } from 'components/forms/FormTextArea'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { LinkTabs } from 'components/LinkTabs'
|
||||
import { cw721BaseLinkTabs } from 'components/LinkTabs.data'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { NextPage } from 'next'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import type { DispatchExecuteArgs } from 'utils/contracts/cw721/base/execute'
|
||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'utils/contracts/cw721/base/execute'
|
||||
import { parseJson } from 'utils/json'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
|
||||
const CW721BaseExecutePage: NextPage = () => {
|
||||
const { cw721Base: contract } = useContracts()
|
||||
const wallet = useWallet()
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
|
||||
const comboboxState = useExecuteComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
|
||||
const contractState = useInputState({
|
||||
id: 'contract-address',
|
||||
name: 'contract-address',
|
||||
title: 'CW721 Contract Address',
|
||||
subtitle: 'Address of the CW721 contract',
|
||||
})
|
||||
|
||||
const messageState = useInputState({
|
||||
id: 'message',
|
||||
name: 'message',
|
||||
title: 'Message',
|
||||
subtitle: 'Message to execute on the contract',
|
||||
defaultValue: JSON.stringify({ key: 'value' }, null, 2),
|
||||
})
|
||||
|
||||
const recipientState = useInputState({
|
||||
id: 'recipient-address',
|
||||
name: 'recipient',
|
||||
title: 'Recipient Address',
|
||||
subtitle: 'Address of the recipient',
|
||||
})
|
||||
|
||||
const tokenIdState = useInputState({
|
||||
id: 'token-id',
|
||||
name: 'token-id',
|
||||
title: 'Token ID',
|
||||
subtitle: 'Identifier of the token',
|
||||
placeholder: 'some_token_id',
|
||||
})
|
||||
|
||||
const showMessageField = type === 'send_nft'
|
||||
const showRecipientField = isEitherType(type, [
|
||||
'transfer_nft',
|
||||
'send_nft',
|
||||
'approve',
|
||||
'revoke',
|
||||
'approve_all',
|
||||
'revoke_all',
|
||||
'mint',
|
||||
])
|
||||
const showTokenIdField = isEitherType(type, ['transfer_nft', 'send_nft', 'approve', 'revoke', 'mint', 'burn'])
|
||||
|
||||
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||
const payload: DispatchExecuteArgs = {
|
||||
contract: contractState.value,
|
||||
messages,
|
||||
msg: parseJson(messageState.value) || {},
|
||||
recipient: recipientState.value,
|
||||
txSigner: wallet.address,
|
||||
type,
|
||||
tokenId: tokenIdState.value,
|
||||
}
|
||||
|
||||
const { isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!type) {
|
||||
throw new Error('Please select message type!')
|
||||
}
|
||||
const txHash = await toast.promise(dispatchExecute(payload), {
|
||||
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
||||
loading: 'Executing message...',
|
||||
success: (tx) => `Transaction ${tx} success!`,
|
||||
})
|
||||
if (txHash) {
|
||||
setLastTx(txHash)
|
||||
}
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
toast.error(String(error))
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
<NextSeo title="Execute CW721 Base Contract" />
|
||||
<ContractPageHeader
|
||||
description="CW721 Base is a specification for non fungible tokens based on CosmWasm."
|
||||
link={links['Docs CW721 Base']}
|
||||
title="CW721 Base Contract"
|
||||
/>
|
||||
<LinkTabs activeIndex={2} data={cw721BaseLinkTabs} />
|
||||
|
||||
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||
<div className="space-y-8">
|
||||
<AddressInput {...contractState} />
|
||||
<ExecuteCombobox {...comboboxState} />
|
||||
{showRecipientField && <AddressInput {...recipientState} />}
|
||||
{showTokenIdField && <AddressInput {...tokenIdState} />}
|
||||
{showMessageField && <JsonTextArea {...messageState} />}
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="relative">
|
||||
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
|
||||
Execute
|
||||
</Button>
|
||||
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
|
||||
<TransactionHash hash={lastTx} />
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
||||
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
|
||||
</FormControl>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(CW721BaseExecutePage, { center: false })
|
1
pages/contracts/cw721/base/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './instantiate'
|
113
pages/contracts/cw721/base/instantiate.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { Alert } from 'components/Alert'
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { LinkTabs } from 'components/LinkTabs'
|
||||
import { cw721BaseLinkTabs } from 'components/LinkTabs.data'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { InstantiateResponse } from 'contracts/cw721/base'
|
||||
import type { NextPage } from 'next'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import type { FormEvent } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { FaAsterisk } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import { CW721_BASE_CODE_ID } from 'utils/constants'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
|
||||
const CW721BaseInstantiatePage: NextPage = () => {
|
||||
const wallet = useWallet()
|
||||
const contract = useContracts().cw721Base
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
placeholder: 'My Awesome CW721 Contract',
|
||||
})
|
||||
|
||||
const symbolState = useInputState({
|
||||
id: 'symbol',
|
||||
name: 'symbol',
|
||||
title: 'Symbol',
|
||||
placeholder: 'AWSM',
|
||||
})
|
||||
|
||||
const minterState = useInputState({
|
||||
id: 'minter-address',
|
||||
name: 'minterAddress',
|
||||
title: 'Minter Address',
|
||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||
})
|
||||
|
||||
const { data, isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent): Promise<InstantiateResponse | null> => {
|
||||
event.preventDefault()
|
||||
if (!contract) {
|
||||
throw new Error('Smart contract connection failed')
|
||||
}
|
||||
const msg = {
|
||||
name: nameState.value,
|
||||
symbol: symbolState.value,
|
||||
minter: minterState.value,
|
||||
}
|
||||
return toast.promise(
|
||||
contract.instantiate(CW721_BASE_CODE_ID, msg, 'StargazeTools CW721 Base Contract', wallet.address),
|
||||
{
|
||||
loading: 'Instantiating contract...',
|
||||
error: 'Instantiation failed!',
|
||||
success: 'Instantiation success!',
|
||||
},
|
||||
)
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(String(error))
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const txHash = data?.transactionHash
|
||||
|
||||
return (
|
||||
<form className="py-6 px-12 space-y-4" onSubmit={mutate}>
|
||||
<NextSeo title="Instantiate CW721 Base Contract" />
|
||||
<ContractPageHeader
|
||||
description="CW721 Base is a specification for non fungible tokens based on CosmWasm."
|
||||
link={links['Docs CW721 Base']}
|
||||
title="CW721 Base Contract"
|
||||
/>
|
||||
<LinkTabs activeIndex={0} data={cw721BaseLinkTabs} />
|
||||
|
||||
<Conditional test={Boolean(data)}>
|
||||
<Alert type="info">
|
||||
<b>Instantiate success!</b> Here is the transaction result containing the contract address and the transaction
|
||||
hash.
|
||||
</Alert>
|
||||
<JsonPreview content={data} title="Transaction Result" />
|
||||
<br />
|
||||
</Conditional>
|
||||
|
||||
<FormGroup subtitle="Basic information about your new contract" title="Contract Details">
|
||||
<TextInput isRequired {...nameState} />
|
||||
<TextInput isRequired {...symbolState} />
|
||||
<TextInput isRequired {...minterState} />
|
||||
</FormGroup>
|
||||
|
||||
<div className="flex items-center p-4">
|
||||
<div className="flex-grow" />
|
||||
<Button isLoading={isLoading} isWide rightIcon={<FaAsterisk />} type="submit">
|
||||
Instantiate Contract
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(CW721BaseInstantiatePage, { center: false })
|
140
pages/contracts/cw721/base/query.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { LinkTabs } from 'components/LinkTabs'
|
||||
import { cw721BaseLinkTabs } from 'components/LinkTabs.data'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useQuery } from 'react-query'
|
||||
import type { QueryType } from 'utils/contracts/cw721/base/query'
|
||||
import { dispatchQuery, QUERY_LIST } from 'utils/contracts/cw721/base/query'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
|
||||
const CW721QueryPage: NextPage = () => {
|
||||
const { cw721Base: contract } = useContracts()
|
||||
const wallet = useWallet()
|
||||
|
||||
const contractState = useInputState({
|
||||
id: 'contract-address',
|
||||
name: 'contract-address',
|
||||
title: 'CW721 contract Address',
|
||||
subtitle: 'Address of the CW721 contract',
|
||||
})
|
||||
const address = contractState.value
|
||||
|
||||
const ownerState = useInputState({
|
||||
id: 'owner-address',
|
||||
name: 'owner-address',
|
||||
title: 'Owner Address',
|
||||
subtitle: 'Address of the owner',
|
||||
})
|
||||
const ownerAddress = ownerState.value
|
||||
|
||||
const tokenIdState = useInputState({
|
||||
id: 'token-id',
|
||||
name: 'token-id',
|
||||
title: 'Token ID',
|
||||
subtitle: 'Identifier of the token',
|
||||
placeholder: 'some_token_id',
|
||||
})
|
||||
const tokenId = tokenIdState.value
|
||||
|
||||
const [type, setType] = useState<QueryType>('owner_of')
|
||||
|
||||
const addressVisible = type === 'approval' || type === 'all_operators' || type === 'tokens'
|
||||
const tokenVisible =
|
||||
type === 'owner_of' || type === 'approval' || type === 'approvals' || type === 'nft_info' || type === 'all_nft_info'
|
||||
|
||||
const { data: response } = useQuery(
|
||||
[address, type, contract, wallet, ownerAddress, tokenId] as const,
|
||||
async ({ queryKey }) => {
|
||||
const [_address, _type, _contract, _wallet, _ownerAddress, _tokenId] = queryKey
|
||||
const messages = contract?.use(_address)
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const ownerAddress = _ownerAddress || _wallet.address
|
||||
|
||||
const result = await dispatchQuery({
|
||||
ownerAddress,
|
||||
tokenId,
|
||||
messages,
|
||||
type,
|
||||
})
|
||||
return result
|
||||
},
|
||||
{
|
||||
placeholderData: null,
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
enabled: Boolean(address && type && contract && wallet),
|
||||
},
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (address.length > 0) {
|
||||
void router.replace({ query: { contractAddress: address } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [address])
|
||||
useEffect(() => {
|
||||
const initial = new URL(document.URL).searchParams.get('contractAddress')
|
||||
if (initial && initial.length > 0) contractState.onChange(initial)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
<NextSeo title="Query CW721 Base Contract" />
|
||||
<ContractPageHeader
|
||||
description="CW721 Base is a specification for non fungible tokens based on CosmWasm."
|
||||
link={links['Docs CW721 Base']}
|
||||
title="CW721 Base Contract"
|
||||
/>
|
||||
<LinkTabs activeIndex={1} data={cw721BaseLinkTabs} />
|
||||
|
||||
<div className="grid grid-cols-2 p-4 space-x-8">
|
||||
<div className="space-y-8">
|
||||
<AddressInput {...contractState} />
|
||||
<FormControl htmlId="contract-query-type" subtitle="Type of query to be dispatched" title="Query Type">
|
||||
<select
|
||||
className={clsx(
|
||||
'bg-white/10 rounded border-2 border-white/20 form-select',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
id="contract-query-type"
|
||||
name="query-type"
|
||||
onChange={(e) => setType(e.target.value as QueryType)}
|
||||
>
|
||||
{QUERY_LIST.map(({ id, name }) => (
|
||||
<option key={`query-${id}`} value={id}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormControl>
|
||||
<Conditional test={addressVisible}>
|
||||
<AddressInput {...ownerState} />
|
||||
</Conditional>
|
||||
<Conditional test={tokenVisible}>
|
||||
<AddressInput {...tokenIdState} />
|
||||
</Conditional>
|
||||
</div>
|
||||
<JsonPreview content={address ? { type, response } : null} title="Query Response" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(CW721QueryPage, { center: false })
|
5
pages/contracts/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
const Minting = () => {
|
||||
return <div>dsadsa</div>
|
||||
}
|
||||
|
||||
export default Minting
|
117
pages/contracts/upload.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import clsx from 'clsx'
|
||||
import { Alert } from 'components/Alert'
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { NextPage } from 'next'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { FaAsterisk } from 'react-icons/fa'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
|
||||
const UploadContract: NextPage = () => {
|
||||
const { getClient, address } = useWallet()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [transactionResult, setTransactionResult] = useState<any>()
|
||||
const [wasmFile, setWasmFile] = useState<File | null>(null)
|
||||
const [wasmByteArray, setWasmByteArray] = useState<Uint8Array | null>(null)
|
||||
|
||||
const inputFile = useRef<HTMLInputElement>(null)
|
||||
|
||||
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files) return
|
||||
setWasmFile(e.target.files[0])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (wasmFile) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
const byteArray = new Uint8Array(e.target.result as ArrayBuffer)
|
||||
setWasmByteArray(byteArray)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
}
|
||||
reader.readAsArrayBuffer(wasmFile)
|
||||
}
|
||||
}, [wasmFile])
|
||||
|
||||
const upload = async () => {
|
||||
try {
|
||||
if (!wasmFile || !wasmByteArray) return toast.error('No file selected.')
|
||||
|
||||
setLoading(true)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const result = await client.upload(address, wasmByteArray, 'auto')
|
||||
|
||||
setTransactionResult({
|
||||
transactionHash: result.transactionHash,
|
||||
codeId: result.codeId,
|
||||
originalSize: result.originalSize,
|
||||
compressedSize: result.compressedSize,
|
||||
originalChecksum: result.originalChecksum,
|
||||
compressedChecksum: result.compressedChecksum,
|
||||
})
|
||||
|
||||
setLoading(false)
|
||||
} catch (err: any) {
|
||||
setLoading(false)
|
||||
toast.error(err.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
<NextSeo title="Upload Contract" />
|
||||
<ContractPageHeader
|
||||
description="Here you can upload a contract on Stargaze Network."
|
||||
link=""
|
||||
title="Upload Contract"
|
||||
/>
|
||||
<div className="inset-x-0 bottom-0 border-b-2 border-white/25" />
|
||||
|
||||
<Conditional test={Boolean(transactionResult)}>
|
||||
<Alert type="info">
|
||||
<b>Upload success!</b> Here is the transaction result containing the code ID, transaction hash and other data.
|
||||
</Alert>
|
||||
<JsonPreview content={transactionResult} title="Transaction Result" />
|
||||
<br />
|
||||
</Conditional>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center space-y-4 h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept=".wasm"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
onChange={onFileChange}
|
||||
ref={inputFile}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pb-6">
|
||||
<Button isDisabled={!wasmFile} isLoading={loading} isWide leftIcon={<FaAsterisk />} onClick={upload}>
|
||||
Upload Contract
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(UploadContract, { center: false })
|
75
pages/index.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { HomeCard } from 'components/HomeCard'
|
||||
import type { NextPage } from 'next'
|
||||
import Brand from 'public/brand/brand.svg'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
|
||||
const HomePage: NextPage = () => {
|
||||
return (
|
||||
<section className="px-8 pt-4 pb-16 mx-auto space-y-8 max-w-4xl">
|
||||
<div className="flex justify-center items-center py-8 max-w-xl">
|
||||
{/* <Brand className="w-full text-plumbus" /> */}
|
||||
</div>
|
||||
<h1 className="font-heading text-4xl font-bold">Welcome!</h1>
|
||||
<p className="text-xl">
|
||||
Looking for a fast and efficient way to build an NFT collection?
|
||||
Stargaze Tools is the solution.
|
||||
<br />
|
||||
<br />
|
||||
Stargaze Tools is built to provide useful smart contract interfaces that
|
||||
helps you build and deploy your own NFT collection in no time.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<HomeCard
|
||||
title="Create"
|
||||
link="/collection/minter"
|
||||
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
||||
>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</HomeCard>
|
||||
<HomeCard
|
||||
title="My Collections"
|
||||
link="/collections"
|
||||
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
||||
>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</HomeCard>
|
||||
{/*
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold">Smart Contract Dashboard</h2>
|
||||
<p className="text-white/75">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dicta
|
||||
asperiores quis soluta recusandae sequi adipisci quod tempora modi,
|
||||
debitis beatae tempore accusantium, esse itaque quaerat obcaecati
|
||||
quia totam necessitatibus voluptas!
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold">Something Cool</h2>
|
||||
<p className="text-white/75">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Fuga
|
||||
accusantium distinctio dignissimos maxime vero illum explicabo
|
||||
officiis. Pariatur magni, enim qui itaque atque quibusdam debitis
|
||||
iste delectus deserunt dolores quisquam?
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold">Adding Value</h2>
|
||||
<p className="text-white/75">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quasi enim
|
||||
minus voluptates dicta, eum debitis iusto commodi delectus itaque
|
||||
qui, unde adipisci. Esse eveniet dolorem consequatur tempore at
|
||||
voluptates aut?
|
||||
</p>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(HomePage, { center: false })
|
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
public/assets/android-chrome-144x144.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
public/assets/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
public/assets/android-chrome-256x256.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
public/assets/android-chrome-36x36.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
public/assets/android-chrome-384x384.png
Normal file
After Width: | Height: | Size: 283 KiB |
BIN
public/assets/android-chrome-48x48.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/assets/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 459 KiB |
BIN
public/assets/android-chrome-72x72.png
Normal file
After Width: | Height: | Size: 33 KiB |