-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #444 from rabbitholegg/mmackz/thirdweb-mint
feat(thirdweb): add support for ThirdWeb `mint` plugin
- Loading branch information
Showing
18 changed files
with
2,937 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.