Skip to content

Commit

Permalink
chore: upload and mint badge from debug
Browse files Browse the repository at this point in the history
  • Loading branch information
1emu committed Aug 28, 2023
1 parent 545bfcf commit e7faa6f
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 73 deletions.
45 changes: 31 additions & 14 deletions src/back/routes/badges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import handleAPI from 'decentraland-gatsby/dist/entities/Route/handle'
import routes from 'decentraland-gatsby/dist/entities/Route/routes'
import { Request } from 'express'

import { ActionStatus, UserBadges, toOtterspaceRevokeReason } from '../../entities/Badges/types'
import { storeBadgeSpec } from '../../entities/Badges/utils'
import { ActionStatus, UploadResult, UserBadges, toOtterspaceRevokeReason } from '../../entities/Badges/types'
import { mintBadge, storeBadgeSpec } from '../../entities/Badges/utils'
import isDebugAddress from '../../entities/Debug/isDebugAddress'
import { BadgesService } from '../../services/BadgesService'
import { AirdropOutcome } from '../models/AirdropJob'
import { validateAddress, validateDate, validateStringNotEmpty } from '../utils/validations'
import { validateAddress, validateDate, validateRequiredString, validateRequiredStrings } from '../utils/validations'

export default routes((router) => {
const withAuth = auth()
router.get('/badges/:address/', handleAPI(getBadges))
router.post('/badges/airdrop/', withAuth, handleAPI(airdropBadges))
router.post('/badges/revoke/', withAuth, handleAPI(revokeBadge))
router.post('/badges/upload/', withAuth, handleAPI(uploadBadge))
router.post('/badges/airdrop/', withAuth, handleAPI(airdrop))
router.post('/badges/revoke/', withAuth, handleAPI(revoke))
router.post('/badges/upload/', withAuth, handleAPI(upload))
router.post('/badges/mint/', withAuth, handleAPI(mint))
})

async function getBadges(req: Request<{ address: string }>): Promise<UserBadges> {
Expand All @@ -25,7 +26,7 @@ async function getBadges(req: Request<{ address: string }>): Promise<UserBadges>
return await BadgesService.getBadges(address)
}

async function airdropBadges(req: WithAuth): Promise<AirdropOutcome> {
async function airdrop(req: WithAuth): Promise<AirdropOutcome> {
const user = req.auth!
const recipients: string[] = req.body.recipients
const badgeSpecCid = req.body.badgeSpecCid
Expand All @@ -45,7 +46,7 @@ async function airdropBadges(req: WithAuth): Promise<AirdropOutcome> {
return await BadgesService.giveBadgeToUsers(badgeSpecCid, recipients)
}

async function revokeBadge(req: WithAuth): Promise<string> {
async function revoke(req: WithAuth): Promise<string> {
const user = req.auth!
const { badgeSpecCid, reason } = req.body
const recipients: string[] = req.body.recipients
Expand Down Expand Up @@ -75,23 +76,39 @@ async function revokeBadge(req: WithAuth): Promise<string> {
}
}

export type UploadResult = { status: ActionStatus; badgeCid: string; error?: string }
async function uploadBadge(req: WithAuth): Promise<UploadResult> {
const user = req.auth!
async function upload(req: WithAuth): Promise<UploadResult> {
const user = req.auth
if (!isDebugAddress(user)) {
throw new RequestError('Invalid user', RequestError.Unauthorized)
}

const { title, description, imgUrl, expiresAt } = req.body
validateStringNotEmpty('title', title) //TODO: refactor so it's one method call that validates chosen fields
validateStringNotEmpty('description', description)
validateStringNotEmpty('imgUrl', imgUrl)
validateRequiredStrings(['title', 'description', 'imgUrl'], req.body)
validateDate(expiresAt)

try {
const result = await storeBadgeSpec(title, description, imgUrl, expiresAt)
return { status: ActionStatus.Success, ...result }
} catch (e) {
console.log('e', e)
return { status: ActionStatus.Failed, badgeCid: JSON.stringify(e) }
}
}

async function mint(req: WithAuth): Promise<UploadResult> {
const user = req.auth
if (!isDebugAddress(user)) {
throw new RequestError('Invalid user', RequestError.Unauthorized)
}

const { badgeCid } = req.body
validateRequiredString('badgeCid', badgeCid)

try {
const result = await mintBadge(badgeCid)
return { status: ActionStatus.Success, badgeCid: JSON.stringify(result) }
} catch (e) {
console.log('e', e)
return { status: ActionStatus.Failed, badgeCid: JSON.stringify(e) }
}
}
12 changes: 10 additions & 2 deletions src/back/utils/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,16 @@ export function validateProposalSnapshotId(proposalSnapshotId?: string) {
}
}

export function validateStringNotEmpty(fieldName: string, someString?: string) {
if (!someString || someString.length === 0) {
export function validateRequiredString(fieldName: string, value?: string) {
if (!value || value.length === 0) {
throw new RequestError(`Invalid ${fieldName}`, RequestError.BadRequest)
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function validateRequiredStrings(fieldNames: string[], requestBody: Record<string, any>) {
for (const fieldName of fieldNames) {
const fieldValue = requestBody[fieldName]
validateRequiredString(fieldName, fieldValue)
}
}
12 changes: 8 additions & 4 deletions src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import env from 'decentraland-gatsby/dist/utils/env'
import snakeCase from 'lodash/snakeCase'

import { AirdropOutcome } from '../back/models/AirdropJob'
import { SpecState } from '../components/Debug/UploadAndMint'
import { SpecState } from '../components/Debug/UploadSpec'
import { GOVERNANCE_API } from '../constants'
import { UserBadges } from '../entities/Badges/types'
import { BidRequest, UnpublishedBidAttributes } from '../entities/Bid/types'
Expand Down Expand Up @@ -626,15 +626,19 @@ export class Governance extends API {
return response.data
}

async uploadAndMint(spec: SpecState) {
async upload(spec: SpecState) {
const response = await this.fetch<ApiResponse<string>>(
`/badges/upload/`,
this.options().method('POST').authorization({ sign: true }).json(spec)
)
return response.data
}

async mint(badgeCid: string | undefined) {
return badgeCid
async mint(badgeCid: string) {
const response = await this.fetch<ApiResponse<string>>(
`/badges/mint/`,
this.options().method('POST').authorization({ sign: true }).json({ badgeCid })
)
return response.data
}
}
22 changes: 17 additions & 5 deletions src/components/Debug/BadgesAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Text from '../Common/Typography/Text'
import ErrorMessage from '../Error/ErrorMessage'
import { ContentSection } from '../Layout/ContentLayout'

import UploadAndMint from './UploadAndMint'
import UploadSpec from './UploadSpec'

interface Props {
className?: string
Expand All @@ -40,9 +40,9 @@ const REVOKE_REASON_OPTIONS = [

export default function BadgesAdmin({ className }: Props) {
const [recipients, setRecipients] = useState<string[]>([])
const [badgeCid, setBadgeCid] = useState<string | undefined>()
const [badgeCid, setBadgeCid] = useState<string>('')
const [reason, setReason] = useState<string>(OtterspaceRevokeReason.TenureEnded)
const [result, setResult] = useState<string | null>()
const [result, setResult] = useState<string>('')
const [errorMessage, setErrorMessage] = useState<string | undefined | null>()
const [formDisabled, setFormDisabled] = useState(false)

Expand All @@ -59,7 +59,16 @@ export default function BadgesAdmin({ className }: Props) {
if (badgeCid && recipients) {
await submit(
async () => Governance.get().revokeBadge(badgeCid, recipients, reason),
(result) => setResult(result)
(result) => setResult(JSON.stringify(result))
)
}
}

async function handleMintBadge() {
if (badgeCid) {
await submit(
async () => Governance.get().mint(badgeCid),
(result) => setResult(JSON.stringify(result))
)
}
}
Expand Down Expand Up @@ -100,6 +109,9 @@ export default function BadgesAdmin({ className }: Props) {
<Button className="Debug__SideButton" primary disabled={formDisabled} onClick={() => handleRevokeBadge()}>
{'Revoke'}
</Button>
<Button className="Debug__SideButton" primary disabled={formDisabled} onClick={() => handleMintBadge()}>
{'Mint'}
</Button>
</div>
<Label>{'Badge Spec Cid'}</Label>
<Field value={badgeCid} onChange={(_, { value }) => setBadgeCid(value)} />
Expand All @@ -123,7 +135,7 @@ export default function BadgesAdmin({ className }: Props) {
</>
)}
</div>
<UploadAndMint />
<UploadSpec />
</ContentSection>
{!!errorMessage && <ErrorMessage label={'Badges Error'} errorMessage={errorMessage} />}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Debug/EnvStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const CONSTANTS: Record<string, any> = {

export default function EnvStatus({ className }: Props) {
const [envName, setEnvName] = useState<string>('')
const [envValue, setEnvValue] = useState<any>()
const [envValue, setEnvValue] = useState<any>('')
const [errorMessage, setErrorMessage] = useState<any>()

async function handleReadVar() {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import { Governance } from '../../clients/Governance'
import useFormatMessage from '../../hooks/useFormatMessage'
import Time from '../../utils/date/Time'
import Field from '../Common/Form/Field'
import SubLabel from '../Common/SubLabel'
import Heading from '../Common/Typography/Heading'
import Label from '../Common/Typography/Label'
import Text from '../Common/Typography/Text'
import ErrorMessage from '../Error/ErrorMessage'
import { ContentSection } from '../Layout/ContentLayout'

import './UploadAndMint.css'
import './UploadSpec.css'

interface Props {
className?: string
Expand All @@ -34,12 +35,10 @@ const initialState: SpecState = {
title: '',
}

export default function UploadAndMint({ className }: Props) {
export default function UploadSpec({ className }: Props) {
const t = useFormatMessage()
const [formDisabled, setFormDisabled] = useState(false)
const [isMinting, setIsMinting] = useState(false)
const [submitError, setSubmitError] = useState('')
const [badgeCid, setBadgeCid] = useState<string | undefined>()
const [result, setResult] = useState<any>()

const {
Expand All @@ -55,42 +54,23 @@ export default function UploadAndMint({ className }: Props) {
setResult(null)
setSubmitError('')
setFormDisabled(true)
console.log('submitting data', JSON.stringify(data))
try {
const result: any = await Governance.get().uploadAndMint(data)
console.log('result', result)
const result: any = await Governance.get().upload(data)
setResult(result)
setBadgeCid(result.badgeCid)
setFormDisabled(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error(err, { ...err })
setSubmitError(err.body?.error || err.message)
setFormDisabled(false)
}
setFormDisabled(false)
}

const onMint = async () => {
setResult(null)
setSubmitError('')
setFormDisabled(true)
setIsMinting(true)
try {
const result: any = await Governance.get().mint(badgeCid)
console.log('result', result)
setResult(result)
} catch (err: any) {
console.error(err, { ...err })
setSubmitError(err.body?.error || err.message)
}
setFormDisabled(false)
setIsMinting(false)
}

return (
<div className={className}>
<form onSubmit={handleSubmit(onSubmit)}>
<ContentSection>
<Heading size="xs">{'Upload and Mint'}</Heading>
<Heading size="xs">{'Upload Badge Spec'}</Heading>
<Label>{'Title'}</Label>
<Field
control={control}
Expand Down Expand Up @@ -144,23 +124,17 @@ export default function UploadAndMint({ className }: Props) {
/>
</ContentSection>

<Button type="submit" className="Debug__SectionButton" primary disabled={formDisabled} loading={isSubmitting}>
{'Upload Spec'}
</Button>
<Button
className="Debug__SectionButton"
primary
disabled={!badgeCid || badgeCid.length === 0}
loading={isMinting}
onClick={onMint}
>
{'Mint'}
</Button>
<div>
<Button type="submit" className="Debug__SectionButton" primary disabled={formDisabled} loading={isSubmitting}>
{'Upload Spec'}
</Button>
</div>
{result && (
<>
<Label>{'Result'}</Label>
<SubLabel>{'Badge Cid'}</SubLabel>
<Text className="Debug__Result">{result.badgeCid}</Text>
<Text className="Debug__Result">{result.metadataUrl}</Text>
<SubLabel>{'IPFS Address'}</SubLabel>
<Text className="Debug__Result">{result.ipfsAddress}</Text>
</>
)}
Expand Down
1 change: 1 addition & 0 deletions src/entities/Badges/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export enum ActionStatus {
}

export type ActionResult = { status: ActionStatus; address: string; badgeId: string; error?: string }
export type UploadResult = { status: ActionStatus; badgeCid: string; error?: string }
export type GasConfig = { gasPrice: ethers.BigNumber; gasLimit: ethers.BigNumber }
export const GAS_MULTIPLIER = 2

Expand Down
Loading

0 comments on commit e7faa6f

Please sign in to comment.