Skip to content

Commit

Permalink
Merge pull request #444 from rabbitholegg/mmackz/thirdweb-mint
Browse files Browse the repository at this point in the history
feat(thirdweb): add support for ThirdWeb `mint` plugin
  • Loading branch information
mmackz authored Jun 12, 2024
2 parents 27f98be + a5e9c0d commit 6b4e034
Show file tree
Hide file tree
Showing 18 changed files with 2,937 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/sharp-gifts-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rabbitholegg/questdk-plugin-registry": minor
"@rabbitholegg/questdk-plugin-thirdweb": minor
---

add thirdweb to plugin registry
3 changes: 2 additions & 1 deletion packages/registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"@rabbitholegg/questdk-plugin-superbridge": "workspace:*",
"@rabbitholegg/questdk-plugin-neynar": "workspace:*",
"@rabbitholegg/questdk-plugin-titles": "workspace:*",
"@rabbitholegg/questdk-plugin-foundation": "workspace:*"
"@rabbitholegg/questdk-plugin-foundation": "workspace:*",
"@rabbitholegg/questdk-plugin-thirdweb": "workspace:*"
}
}
2 changes: 2 additions & 0 deletions packages/registry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { Vela } from '@rabbitholegg/questdk-plugin-vela'
import { WooFi } from '@rabbitholegg/questdk-plugin-woofi'
import { Zora } from '@rabbitholegg/questdk-plugin-zora'
import { Foundation } from '@rabbitholegg/questdk-plugin-foundation'
import { ThirdWeb } from '@rabbitholegg/questdk-plugin-thirdweb'
// ^^^ New Imports Go Here ^^^
import {
ActionType,
Expand Down Expand Up @@ -115,6 +116,7 @@ export const plugins: Record<string, IActionPlugin> = {
[Neynar.pluginId]: Neynar,
[Titles.pluginId]: Titles,
[Foundation.pluginId]: Foundation,
[ThirdWeb.pluginId]: ThirdWeb,
}

export const getPlugin = (pluginId: string) => {
Expand Down
21 changes: 21 additions & 0 deletions packages/thirdweb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## ThirdWeb

Thirdweb is a robust platform that streamlines the process of minting and managing NFTs. By providing easy-to-use tools and infrastructure, it empowers creators to mint, deploy, and manage their NFT collections efficiently.

## Mint Plugin

### Implementation Details

Thirdweb is available on most major L2 networks such as base and optimism

We have support for two types of contracts, `DropERC1155` and `OpenEditionERC721`, both are quite similar, with the difference being the ERC1155 can utilize a tokenId parameter. Both use the `claim` functions on their respective contracts.

### Sample Mints

- ERC721: https://wallet.coinbase.com/nft/mint/MDP
- ERC1155: https://basescan.org/token/0x5625e0ae98c035407258d6752703fed917417add

### Example Transactions

- ERC721: https://basescan.org/tx/0x16ed42066507832a5452dd1c012b1c6f694284987abcd2d8b3d2e0cdc7eb9678
- ERC1155: https://basescan.org/tx/0x2f0bba62facc408f510a1ca52e9e41f09e067646854ceda1c4e379caca6c946d
36 changes: 36 additions & 0 deletions packages/thirdweb/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@rabbitholegg/questdk-plugin-thirdweb",
"private": false,
"version": "1.0.0-alpha.2",
"description": "Plugin for ThirdWeb",
"exports": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"typings": "./dist/types/index.d.ts",
"scripts": {
"build": "vite build && tsc --project tsconfig.build.json --emitDeclarationOnly --declaration --declarationMap --declarationDir ./dist/types",
"bench": "vitest bench",
"bench:ci": "CI=true vitest bench",
"clean": "rimraf dist",
"format": "rome format . --write",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest dev",
"test:cov": "vitest dev --coverage",
"test:ci": "CI=true vitest --coverage",
"test:ui": "vitest dev --ui"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {},
"dependencies": {
"@rabbitholegg/questdk-plugin-utils": "workspace:*",
"@rabbitholegg/questdk": "workspace:*"
}
}
249 changes: 249 additions & 0 deletions packages/thirdweb/src/ThirdWeb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { mint } from './ThirdWeb'
import { failingTestCases, passingTestCases } from './test-transactions'
import { apply } from '@rabbitholegg/questdk'
import {
Chains,
type MintActionParams,
type MintIntentParams,
} from '@rabbitholegg/questdk-plugin-utils'
import { type Address, parseEther } from 'viem'
import { describe, expect, test, vi } from 'vitest'

describe('Given the thirdweb plugin', () => {
describe('When handling the mint action', () => {
describe('should return a valid action filter', () => {
test('when making a valid mint action', async () => {
const filter = await mint({
chainId: 8453,
contractAddress: '0xc7DeD9c1BD13A19A877d196Eeea9222Ff6d40736',
})
expect(filter).toBeTypeOf('object')
expect(Number(filter.chainId)).toBe(8453)
if (typeof filter.to === 'string') {
expect(filter.to).toMatch(/^0x[a-fA-F0-9]{40}$/)
} else {
// if to is an object, it should have a logical operator as the only key
expect(filter.to).toBeTypeOf('object')
expect(Object.keys(filter.to)).toHaveLength(1)
expect(
['$or', '$and'].some((prop) =>
Object.hasOwnProperty.call(filter.to, prop),
),
).to.be.true
expect(Object.values(filter.to)[0]).to.satisfy((arr: string[]) =>
arr.every((val) => val.match(/^0x[a-fA-F0-9]{40}$/)),
)
}
// Check the input property is the correct type and has a valid filter operator
expect(filter.input).toBeTypeOf('object')
expect(
['$abi', '$abiParams', '$abiAbstract', '$or', '$and'].some((prop) =>
Object.hasOwnProperty.call(filter.input, prop),
),
).to.be.true
})
})

describe('should pass filter with valid transactions', () => {
passingTestCases.forEach((testCase) => {
const { transaction, description, params } = testCase
test(description, async () => {
const filter = await mint(params)
expect(apply(transaction, filter)).to.be.true
})
})
})

describe('should not pass filter with invalid transactions', () => {
failingTestCases.forEach((testCase) => {
const { transaction, description, params } = testCase
test(description, async () => {
try {
const filter = await mint(params)
const result = apply(transaction, filter)
expect(result).toBe(false)
} catch (error) {
expect(error).toBeDefined()
}
})
})
})
})

describe('Given the getFee function', () => {
test('should return the correct fee for OpenEditionERC721 mint', async () => {
const contractAddress: Address =
'0xc7DeD9c1BD13A19A877d196Eeea9222Ff6d40736'
const mintParams = {
contractAddress,
chainId: Chains.BASE,
}

// mock
const mockFns = {
getFees: async (_mint: MintActionParams) => ({
projectFee: parseEther('0'),
actionFee: parseEther('0.000777'),
}),
}
const getProjectsFeeSpy = vi.spyOn(mockFns, 'getFees')
const fee = await mockFns.getFees(mintParams)
expect(getProjectsFeeSpy).toHaveBeenCalledWith(mintParams)
expect(fee.projectFee).toEqual(parseEther('0'))
expect(fee.actionFee).toEqual(parseEther('0.000777'))
})

test('should return the correct fee for DropERC1155 mint', async () => {
const contractAddress: Address =
'0x5625e0ae98C035407258D6752703fed917417Add'
const mintParams = {
contractAddress,
chainId: Chains.BASE,
tokenId: 0,
}

// mock
const mockFns = {
getFees: async (_mint: MintActionParams) => ({
projectFee: parseEther('0'),
actionFee: parseEther('0.000777'),
}),
}
const getProjectsFeeSpy = vi.spyOn(mockFns, 'getFees')
const fee = await mockFns.getFees(mintParams)
expect(getProjectsFeeSpy).toHaveBeenCalledWith(mintParams)
expect(fee.projectFee).toEqual(parseEther('0'))
expect(fee.actionFee).toEqual(parseEther('0.000777'))
})

test('should return the fallback fee if contract not found or error occurs', async () => {
const contractAddress: Address =
'0x68adf0c109e63c6141c509fea0864431ba55bfa5'
const mintParams = {
contractAddress,
chainId: Chains.BASE,
}

// mock
const mockFns = {
getFees: async (_mint: MintActionParams) => ({
projectFee: parseEther('0'),
actionFee: parseEther('0'),
}),
}
const getFeesSpy = vi.spyOn(mockFns, 'getFees')
const fee = await mockFns.getFees(mintParams)
expect(getFeesSpy).toHaveBeenCalledWith(mintParams)
expect(fee.projectFee).toEqual(parseEther('0'))
expect(fee.actionFee).toEqual(parseEther('0'))
})
})

describe('simulateMint function', () => {
test('should simulate an 1155 mint', async () => {
const mint = {
chainId: Chains.BASE,
contractAddress: '0x5625e0ae98C035407258D6752703fed917417Add',
recipient: '0xf70da97812CB96acDF810712Aa562db8dfA3dbEF',
tokenId: 0,
}
const value = parseEther('0.000777')
const address = mint.recipient as Address

// mock
const mockFns = {
simulateMint: async (
_mint: MintIntentParams,
_value: bigint,
_address: Address,
) => ({
request: {
address: '0x5625e0ae98C035407258D6752703fed917417Add',
functionName: 'claim',
value: 777000000000000n,
},
}),
}
const simulateMintSpy = vi.spyOn(mockFns, 'simulateMint')
const result = await mockFns.simulateMint(
mint as MintIntentParams,
value,
address,
)
expect(simulateMintSpy).toHaveBeenCalledWith(
mint as MintIntentParams,
value,
address,
)

const request = result.request
expect(request.address).toBe('0x5625e0ae98C035407258D6752703fed917417Add')
expect(request.functionName).toBe('claim')
expect(request.value).toBe(value)
})

test('should simulate a 721 mint', async () => {
const mint = {
chainId: Chains.BASE,
contractAddress: '0xc7ded9c1bd13a19a877d196eeea9222ff6d40736',
recipient: '0xf70da97812CB96acDF810712Aa562db8dfA3dbEF',
}
const value = parseEther('0.000777')
const address = mint.recipient as Address

// mock
const mockFns = {
simulateMint: async (
_mint: MintIntentParams,
_value: bigint,
_address: Address,
) => ({
request: {
address: '0xc7ded9c1bd13a19a877d196eeea9222ff6d40736',
functionName: 'claim',
value: 777000000000000n,
},
}),
}
const simulateMintSpy = vi.spyOn(mockFns, 'simulateMint')
const result = await mockFns.simulateMint(
mint as MintIntentParams,
value,
address,
)
expect(simulateMintSpy).toHaveBeenCalledWith(
mint as MintIntentParams,
value,
address,
)

const request = result.request
expect(request.address).toBe('0xc7ded9c1bd13a19a877d196eeea9222ff6d40736')
expect(request.functionName).toBe('claim')
expect(request.value).toBe(value)
})

test('should fail to simulate with invalid parameters', async () => {
const mint = {
chainId: Chains.ETHEREUM,
contractAddress: '0xc7ded9c1bd13a19a877d196eeea9222ff6d40736',
recipient: '0xf70da97812CB96acDF810712Aa562db8dfA3dbEF',
}
const value = parseEther('0.000777')
const address = mint.recipient as Address

// mock
const mockFns = {
simulateMint: async (
_mint: MintIntentParams,
_value: bigint,
_address: Address,
) => {},
}
vi.spyOn(mockFns, 'simulateMint').mockRejectedValue(new Error())
await expect(
mockFns.simulateMint(mint as MintIntentParams, value, address),
).rejects.toThrow()
})
})
})
Loading

0 comments on commit 6b4e034

Please sign in to comment.