Skip to content

feat: add zora premint #446

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/mighty-waves-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rabbitholegg/questdk-plugin-utils": patch
"@rabbitholegg/questdk-plugin-zora": patch
---

add zora premint
7 changes: 7 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export type {
PluginActionPayload,
PluginActionValidation,
QuestCompletionPayload,
PremintActionParams,
PremintValidationParams,
PremintActionDetail,
PremintActionForm,
} from './types'

export {
Expand Down Expand Up @@ -126,6 +130,9 @@ export {
ValidationParamsSchema,
ActionValidationSchema,
PluginActionValidationSchema,
PremintValidationParamsSchema,
PremintActionDetailSchema,
PremintActionFormSchema,
} from './types'

export { PluginActionNotImplementedError } from './errors'
38 changes: 38 additions & 0 deletions packages/utils/src/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type ActionParams =
| CreateActionParams
| CompleteActionParams
| CollectActionParams
| PremintActionParams

export type DisctriminatedActionParams =
| { type: ActionType.Swap; data: SwapActionParams }
Expand Down Expand Up @@ -288,6 +289,33 @@ export const RecastActionFormSchema = z.object({
})
export type RecastActionForm = z.infer<typeof RecastActionFormSchema>

/*
PREMINT
*/
export type PremintActionParams = {
chainId: number
createdAfter: string
}

export const PremintValidationParamsSchema = z.object({
actor: z.string(),
})
export type PremintValidationParams = z.infer<
typeof PremintValidationParamsSchema
>

export const PremintActionDetailSchema = z.object({
chainId: z.number(),
createdAfter: z.string().datetime(),
})
export type PremintActionDetail = z.infer<typeof PremintActionDetailSchema>

export const PremintActionFormSchema = z.object({
chainId: z.number(),
createdAfter: z.string().datetime(),
})
export type PremintActionForm = z.infer<typeof PremintActionFormSchema>

/*
CREATE
*/
Expand Down Expand Up @@ -428,6 +456,7 @@ export const ActionParamsSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('create'), data: CreateActionDetailSchema }),
z.object({ type: z.literal('complete'), data: CompleteActionDetailSchema }),
z.object({ type: z.literal('collect'), data: CollectActionDetailSchema }),
z.object({ type: z.literal('premint'), data: PremintActionDetailSchema }),
])

export const QuestActionParamsSchema = ActionParamsSchema
Expand All @@ -440,6 +469,10 @@ export const ValidationParamsSchema = z.discriminatedUnion('type', [
data: CompleteValidationParamsSchema,
}),
z.object({ type: z.literal('collect'), data: CollectValidationParamsSchema }),
z.object({
type: z.literal('premint'),
data: PremintValidationParamsSchema,
}),
])

export type ValidationParams = z.infer<typeof ValidationParamsSchema>
Expand Down Expand Up @@ -532,6 +565,10 @@ export interface IActionPlugin {
actionP: CollectActionParams,
validateP: CollectValidationParams,
) => Promise<boolean> | Promise<PluginActionNotImplementedError>
validatePremint?: (
actionP: PremintActionParams,
validateP: PremintValidationParams,
) => Promise<boolean> | Promise<PluginActionNotImplementedError>
canValidate?: (actionType: ActionType) => boolean
}

Expand Down Expand Up @@ -596,6 +633,7 @@ export enum ActionType {
Create = 'create',
Complete = 'complete',
Collect = 'collect',
Premint = 'premint',
}

export enum OrderType {
Expand Down
7 changes: 7 additions & 0 deletions packages/utils/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export type {
ActionValidation,
PluginActionPayload,
PluginActionValidation,
PremintActionParams,
PremintValidationParams,
PremintActionDetail,
PremintActionForm,
} from './actions'

export {
Expand Down Expand Up @@ -84,6 +88,9 @@ export {
ValidationParamsSchema,
ActionValidationSchema,
PluginActionValidationSchema,
PremintActionFormSchema,
PremintActionDetailSchema,
PremintValidationParamsSchema,
} from './actions'

export {
Expand Down
161 changes: 146 additions & 15 deletions packages/zora/src/Zora.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,71 @@ import {
type DisctriminatedActionParams,
type MintActionParams,
type MintIntentParams,
type PremintActionParams,
} from '@rabbitholegg/questdk-plugin-utils'
import { apply } from '@rabbitholegg/questdk/filter'
import { apply } from '@rabbitholegg/questdk'
import { type Address, parseEther } from 'viem'
import { describe, expect, test, vi } from 'vitest'
import { describe, expect, test, vi, beforeEach, MockedFunction } from 'vitest'
import { PremintResponse } from './types'
import axios from 'axios'
import { validatePremint } from './validate'

const MockedPremintResponse: PremintResponse = [
{
zoraV2: {
collection: {
contractAdmin: '0xd272a3cb66bea1fa7547dad5b420d5ebe14222e5',
contractURI:
'ipfs://bafkreicuxlqqgoo6fxlmijqvilckvwj6ey26yvzpwg73ybcltvvek2og6i',
contractName: 'Fancy title',
},
premint: {
tokenConfig: {
tokenURI:
'ipfs://bafkreia474gkk2ak5eeqstp43nqeiunqkkfeblctna3y54av7bt6uwehmq',
maxSupply: '0xffffffffffffffff',
maxTokensPerAddress: 0,
pricePerToken: 0,
mintStart: 1708100240,
mintDuration: 2592000,
royaltyBPS: 500,
payoutRecipient: '0xd272a3cb66bea1fa7547dad5b420d5ebe14222e5',
fixedPriceMinter: '0x04e2516a2c207e84a1839755675dfd8ef6302f0a',
createReferral: '0x0000000000000000000000000000000000000000',
},
uid: 1,
version: 1,
deleted: false,
},
collectionAddress: '0x0cfbce0e2ea475d6413e2f038b2b62e64106ad1f',
chainId: 7777777,
signature:
'0x2eb4d27a5b04fd41bdd33f66a18a4993c0116724c5fe5b8dc20bf22f45455c621139eabdbd27434e240938a60b1952979c9dc9c8a141cc71764786fe4d3f909f1c',
},
},
]

vi.mock('axios', () => {
return {
default: {
post: vi.fn(),
get: vi.fn(),
delete: vi.fn(),
put: vi.fn(),
create: vi.fn().mockReturnThis(),
interceptors: {
request: {
use: vi.fn(),
eject: vi.fn(),
},
response: {
use: vi.fn(),
eject: vi.fn(),
},
},
},
}
})

describe('Given the zora plugin', () => {
describe('When handling the mint action', () => {
Expand Down Expand Up @@ -121,6 +182,72 @@ describe('Given the zora plugin', () => {
})
})
})

describe('when handling the premint action', () => {
beforeEach(() => {
vi.resetAllMocks()
})

describe('validatePremint function', () => {
test('should return true if actor has preminted after the specified time', async () => {
const createdAfter = new Date('2024-06-10T12:00:00.000Z')
;(axios.get as MockedFunction<typeof axios.get>).mockResolvedValue({
status: 200,
data: MockedPremintResponse,
})
const actor = '0xd272a3cb66bea1fa7547dad5b420d5ebe14222e5'
const actionParams: PremintActionParams = {
chainId: 7777777,
createdAfter: createdAfter.toISOString(),
}
const result = await validatePremint(actionParams, { actor })

expect(result).to.be.true
})

test('should return true if actor has preminted with a specified chain id', async () => {
const createdAfter = new Date('2024-06-10T12:00:00.000Z').toISOString()
const chainId = 7777777
;(axios.get as MockedFunction<typeof axios.get>).mockResolvedValue({
status: 200,
data: MockedPremintResponse,
})
const actor = '0xd272a3cb66bea1fa7547dad5b420d5ebe14222e5'
const actionParams: PremintActionParams = { chainId, createdAfter }
const result = await validatePremint(actionParams, { actor })

expect(result).to.be.true
})

test('should return false if actor has not preminted', async () => {
const createdAfter = new Date('2024-06-10T12:00:00.000Z').toISOString()
const chainId = 7777777
;(axios.get as MockedFunction<typeof axios.get>).mockResolvedValue({
status: 200,
data: [],
})
const actor = '0x000000000000000000000000000000000000dead'
const actionParams: PremintActionParams = { createdAfter, chainId }
const result = await validatePremint(actionParams, { actor })

expect(result).to.be.false
})

test('should return false if actor has not preminted after the specified time', async () => {
const createdAfter = new Date('2024-06-10T12:00:00.000Z').toISOString()
const chainId = 7777777
;(axios.get as MockedFunction<typeof axios.get>).mockResolvedValue({
status: 200,
data: [],
})
const actor = '0xd272a3cb66bea1fa7547dad5b420d5ebe14222e5'
const actionParams: PremintActionParams = { createdAfter, chainId }
const result = await validatePremint(actionParams, { actor })

expect(result).to.be.false
})
})
})
})

describe('Given the getMintIntent function', () => {
Expand Down Expand Up @@ -252,21 +379,25 @@ describe('Given the getFee function', () => {
})

describe('simulateMint function', () => {
const mockFn = {
simulateMint: async (
_mint: MintIntentParams,
_value: bigint,
_account: Address,
) => ({
request: {
address: '0x5F69dA5Da41E5472AfB88fc291e7a92b7F15FbC5',
value: parseEther('0.000777'),
},
}),
}
vi.spyOn(mockFn, 'simulateMint')
beforeEach(() => {
vi.resetAllMocks()
})

test('should simulate a 1155 mint when tokenId is not 0', async () => {
const mockFn = {
simulateMint: async (
_mint: MintIntentParams,
_value: bigint,
_account: Address,
) => ({
request: {
address: '0x5F69dA5Da41E5472AfB88fc291e7a92b7F15FbC5',
value: parseEther('0.000777'),
},
}),
}
vi.spyOn(mockFn, 'simulateMint')

const mint: MintIntentParams = {
chainId: Chains.BASE,
contractAddress: '0x5F69dA5Da41E5472AfB88fc291e7a92b7F15FbC5',
Expand Down
29 changes: 29 additions & 0 deletions packages/zora/src/Zora.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ZORA_1155_FACTORY,
ZORA_DEPLOYER_ADDRESS,
} from './contract-addresses'
import { validatePremint } from './validate'
import {
type MintActionParams,
type CreateActionParams,
Expand All @@ -25,6 +26,8 @@ import {
type MintIntentParams,
chainIdToViemChain,
getExitAddresses,
PluginActionValidation,
QuestCompletionPayload,
} from '@rabbitholegg/questdk-plugin-utils'
import { MintAPIClient, getMintCosts } from '@zoralabs/protocol-sdk'
import { zoraUniversalMinterAddress } from '@zoralabs/universal-minter'
Expand All @@ -45,6 +48,32 @@ import {
toHex,
} from 'viem'

export const validate = async (
validationPayload: PluginActionValidation,
): Promise<QuestCompletionPayload | null> => {
const { actor, payload } = validationPayload
const { actionParams, validationParams, questId, taskId } = payload
switch (actionParams.type) {
case ActionType.Premint: {
const isPremintValid = await validatePremint(
actionParams.data,
validationParams.data,
)
if (isPremintValid) {
return {
address: actor,
questId,
taskId,
}
}

return null
}
default:
throw new Error('Unsupported action type')
}
}

export const create = async (
create: CreateActionParams,
): Promise<TransactionFilter> => {
Expand Down
4 changes: 4 additions & 0 deletions packages/zora/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
getSupportedTokenAddresses,
mint,
simulateMint,
validate,
} from './Zora'
import { validatePremint } from './validate'

export const Zora: IActionPlugin = {
pluginId: 'zora',
Expand All @@ -32,4 +34,6 @@ export const Zora: IActionPlugin = {
getFees(params as unknown as MintActionParams),
getMintIntent,
simulateMint,
validate,
validatePremint,
}
Loading
Loading