Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
90ecf31
refactor(titles): update create test transactions
mmackz Jul 19, 2024
512a2fd
feat(titles): add abi for V2 mints
mmackz Jul 19, 2024
85b44a1
test(titles): add tests for mint action
mmackz Jul 19, 2024
f89fe3d
feat(titles): export TITLES_COLLECTION_ABI_V2 constant
mmackz Jul 19, 2024
67349ec
test(titles): add tests for amount
mmackz Jul 19, 2024
7338758
test(titles): add tests for mint action validation
mmackz Jul 19, 2024
16f580d
feat(titles): add mint validation filter
mmackz Jul 19, 2024
2b24dcc
chore: format
mmackz Jul 19, 2024
5d06a1f
feat(titles): add getclient function
mmackz Jul 19, 2024
3b3764d
feat(titles): add mintfee abi
mmackz Jul 19, 2024
cca557d
test(titles): add test for getFee function
mmackz Jul 19, 2024
b4260dc
feat(titles): implement getFees and getProjectFees
mmackz Jul 19, 2024
b910474
chore: format
mmackz Jul 19, 2024
77a0949
test(titles): add test for simulateMint
mmackz Jul 19, 2024
fe4dffc
feat(titles): implement simulatemint function
mmackz Jul 19, 2024
fd52c97
chore: merge
mmackz Jul 23, 2024
064a5ec
Revert "chore: merge"
mmackz Jul 23, 2024
9b8d8aa
refactor: use updated utils functions
mmackz Jul 23, 2024
eb2f10e
feat(titles): add CHAIN_ID_TO_SLUG constant
mmackz Jul 23, 2024
9eff822
feat(titles): implement getExternalUrl
mmackz Jul 23, 2024
b79d760
test(titles): add test for getExternalUrl
mmackz Jul 23, 2024
f615ff8
feat(titles): implement mint intents
mmackz Jul 23, 2024
617bdc7
chore: format
mmackz Jul 23, 2024
a02eb7b
feat(titles): export plugin functions
mmackz Jul 23, 2024
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
167 changes: 163 additions & 4 deletions packages/titles/src/Titles.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { create } from './Titles'
import { failingTestCases, passingTestCases } from './test-transactions'
import {
create,
getExternalUrl,
getFees,
getMintIntent,
mint,
simulateMint,
} from './Titles'
import {
failingTestCasesCreate,
failingTestCasesMint,
passingTestCasesCreate,
passingTestCasesMint,
} from './test-transactions'
import { apply } from '@rabbitholegg/questdk'
import {
Chains,
type MintIntentParams,
} from '@rabbitholegg/questdk-plugin-utils'
import { parseEther, type Address } from 'viem'
import { describe, expect, test } from 'vitest'

describe('Given the titles plugin', () => {
Expand Down Expand Up @@ -31,7 +48,7 @@ describe('Given the titles plugin', () => {
})

describe('should pass filter with valid transactions', () => {
passingTestCases.forEach((testCase) => {
passingTestCasesCreate.forEach((testCase) => {
const { transaction, description, params } = testCase
test(description, async () => {
const filter = await create(params)
Expand All @@ -41,7 +58,7 @@ describe('Given the titles plugin', () => {
})

describe('should not pass filter with invalid transactions', () => {
failingTestCases.forEach((testCase) => {
failingTestCasesCreate.forEach((testCase) => {
const { transaction, description, params } = testCase
test(description, async () => {
const filter = await create(params)
Expand All @@ -50,4 +67,146 @@ describe('Given the titles 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: '0x04e4d53374a5e6259ce06cfc6850a839bd960d01',
})
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}$/)),
)
}
})
})

describe('should pass filter with valid transactions', () => {
passingTestCasesMint.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', () => {
failingTestCasesMint.forEach((testCase) => {
const { transaction, description, params } = testCase
test(description, async () => {
const filter = await mint(params)
expect(apply(transaction, filter)).to.be.false
})
})
})
})
})

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

const fee = await getFees(mintParams)
expect(fee.projectFee).toEqual(parseEther('0.0005'))
expect(fee.actionFee).toEqual(parseEther('0'))
})
})

describe('simulateMint function', () => {
test('should simulate a V2 mint', async () => {
const contractAddress: Address =
'0x432f4Ccc39AB8DD8015F590a56244bECb8D16933'
const mintParams = {
contractAddress,
chainId: Chains.BASE,
tokenId: 4,
recipient: '0xf70da97812CB96acDF810712Aa562db8dfA3dbEF',
}
const value = parseEther('0.0005')
const address = mintParams.recipient as Address

const result = await simulateMint(
mintParams as MintIntentParams,
value,
address,
)

const request = result.request
expect(request.address).toBe('0x432f4Ccc39AB8DD8015F590a56244bECb8D16933')
expect(request.functionName).toBe('mint')
expect(request.value).toBe(value)
})

test('should simulate a V2 mint', async () => {
const contractAddress: Address =
'0x06d7D870a41a44B5b7eBF46019bD5f8487362de3'
const mintParams = {
contractAddress,
chainId: Chains.BASE,
tokenId: 5,
recipient: '0xf70da97812CB96acDF810712Aa562db8dfA3dbEF',
}
const value = parseEther('0.0005')
const address = mintParams.recipient as Address

const result = await simulateMint(
mintParams as MintIntentParams,
value,
address,
)

const request = result.request
expect(request.address).toBe('0x06d7D870a41a44B5b7eBF46019bD5f8487362de3')
expect(request.functionName).toBe('mint')
expect(request.value).toBe(value)
})
})

describe('getExternalUrl function', () => {
test('should return the correct URL for a V2 mint', () => {
const url = getExternalUrl({
chainId: Chains.BASE,
contractAddress: '0x432f4ccc39ab8dd8015f590a56244becb8d16933',
tokenId: 1,
})
expect(url).toEqual(
'https://titles.xyz/collect/base/0x432f4ccc39ab8dd8015f590a56244becb8d16933/1',
)
})
})

describe('getMintIntent function', () => {
test('should return the correct mint intent for a V2 mint', async () => {
const mintIntent = await getMintIntent({
chainId: Chains.BASE,
contractAddress: '0x432f4ccc39ab8dd8015f590a56244becb8d16933',
tokenId: 1,
recipient: '0xf70da97812CB96acDF810712Aa562db8dfA3dbEF',
amount: BigInt(1),
})
expect(mintIntent.data).toEqual(
'0x972f9e94000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000e3bba2a4f8e0f5c32ef5097f988a4d88075c8b4800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000',
)
})
})
160 changes: 157 additions & 3 deletions packages/titles/src/Titles.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
import { TITLES_ABI_V1, TITLES_PUBLISHER_V1 } from './constants'
import { getClient } from './client'
import {
MINT_FEE_ABI,
TITLES_ABI_V1,
TITLES_PUBLISHER_V1,
TITLES_COLLECTION_ABI_V2,
CHAIN_ID_TO_SLUG,
} from './constants'
import {
type CreateActionParams,
type TransactionFilter,
compressJson,
} from '@rabbitholegg/questdk'
import { Chains } from '@rabbitholegg/questdk-plugin-utils'
import { type Address } from 'viem'
import {
Chains,
DEFAULT_ACCOUNT,
DEFAULT_REFERRAL,
MintActionParams,
MintIntentParams,
formatAmountToFilterOperator,
formatAmountToInteger,
} from '@rabbitholegg/questdk-plugin-utils'
import {
type Address,
type PublicClient,
type TransactionRequest,
type SimulateContractReturnType,
encodeFunctionData,
parseEther,
zeroHash,
} from 'viem'

export const create = async (
create: CreateActionParams,
Expand All @@ -20,6 +43,137 @@ export const create = async (
})
}

export const mint = async (
mint: MintActionParams,
): Promise<TransactionFilter> => {
const { chainId, contractAddress, amount, tokenId, recipient, referral } =
mint

return compressJson({
chainId,
to: contractAddress,
input: {
$abi: TITLES_COLLECTION_ABI_V2,
to_: recipient,
tokenId_: tokenId,
amount_: formatAmountToFilterOperator(amount),
referrer_: referral,
},
})
}

export const getProjectFees = async (
mint: MintActionParams,
): Promise<bigint> => {
const fees = await getFees(mint)
return fees.projectFee + fees.actionFee
}

export const getFees = async (
mint: MintActionParams,
): Promise<{ actionFee: bigint; projectFee: bigint }> => {
const { chainId, contractAddress, amount, tokenId } = mint
const quantityToMint = formatAmountToInteger(amount)

if (tokenId == null) {
throw new Error('Token ID is required')
}

try {
const client = getClient(chainId)
const mintFee = (await client.readContract({
address: contractAddress,
abi: MINT_FEE_ABI,
functionName: 'mintFee',
args: [tokenId],
})) as bigint

return {
actionFee: 0n,
projectFee: mintFee * quantityToMint,
}
} catch (error) {
// return fallback if any errors occur
// default mint fee is 0.0005 ETH
console.error(error)
return {
actionFee: 0n,
projectFee: parseEther('0.0005') * quantityToMint,
}
}
}

export const getMintIntent = async (
mint: MintIntentParams,
): Promise<TransactionRequest> => {
const { contractAddress, recipient, tokenId, amount, referral } = mint
const quantity = formatAmountToInteger(amount)

const mintArgs = [
recipient,
tokenId,
quantity,
referral ?? DEFAULT_REFERRAL,
zeroHash,
]

const data = encodeFunctionData({
abi: TITLES_COLLECTION_ABI_V2,
functionName: 'mint',
args: mintArgs,
})

return {
from: recipient,
to: contractAddress,
data,
}
}

export const simulateMint = async (
mint: MintIntentParams,
value: bigint,
account?: Address,
_client?: PublicClient,
): Promise<SimulateContractReturnType> => {
const { chainId, contractAddress, amount, recipient, tokenId, referral } =
mint

const client = _client || getClient(chainId)

if (tokenId == null) {
throw new Error('Token ID is required')
}

const mintArgs = [
recipient,
tokenId,
formatAmountToInteger(amount),
referral ?? DEFAULT_REFERRAL,
zeroHash,
]

const result = await client.simulateContract({
address: contractAddress,
value,
abi: TITLES_COLLECTION_ABI_V2,
functionName: 'mint',
args: mintArgs,
account: account ?? DEFAULT_ACCOUNT,
})
return result
}

export function getExternalUrl(params: MintActionParams): string {
const { chainId, contractAddress, tokenId } = params
const slug = CHAIN_ID_TO_SLUG[chainId]

if (!slug || !contractAddress || !tokenId) {
return 'https://titles.xyz'
}
return `https://titles.xyz/collect/${slug}/${contractAddress}/${tokenId}`
}

export const getSupportedTokenAddresses = async (
_chainId: number,
): Promise<Address[]> => {
Expand Down
11 changes: 11 additions & 0 deletions packages/titles/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { chainIdToViemChain } from '@rabbitholegg/questdk-plugin-utils'
import { type PublicClient, createPublicClient, http } from 'viem'

export function getClient(chainId: number) {
const client = createPublicClient({
chain: chainIdToViemChain(chainId),
transport: http(),
}) as PublicClient

return client
}
Loading