diff --git a/src/lib/nftClaimer/mint.ts b/src/lib/nftClaimer/mint.ts index 4e8f17d..39b8956 100644 --- a/src/lib/nftClaimer/mint.ts +++ b/src/lib/nftClaimer/mint.ts @@ -7,7 +7,8 @@ import { numberizeProposalId, validateMintInput, mintingAllowed, - hasVoted + hasVoted, + hasMinted } from './utils'; import abi from './spaceCollectionImplementationAbi.json'; import { FormatTypes, Interface } from '@ethersproject/abi'; @@ -44,6 +45,10 @@ export default async function payload(input: { throw new Error('Minting is open only for voters'); } + if (await hasMinted(params.recipient, proposal as Proposal)) { + throw new Error('You can only mint once per vote'); + } + const message = { proposer: params.proposalAuthor, recipient: params.recipient, diff --git a/src/lib/nftClaimer/utils.ts b/src/lib/nftClaimer/utils.ts index c400d8c..3bbd2e2 100644 --- a/src/lib/nftClaimer/utils.ts +++ b/src/lib/nftClaimer/utils.ts @@ -48,6 +48,11 @@ export async function hasVoted(address: string, proposal: Proposal) { return vote !== undefined; } +export async function hasMinted(address: string, proposal: Proposal) { + const mint = await getMint(address, proposal.id); + return mint !== undefined; +} + export async function validateSpace(address: string, space: Space | null) { if (!space) { throw new Error('RECORD_NOT_FOUND'); @@ -133,6 +138,32 @@ export async function getSpaceCollection(spaceId: string) { return spaceCollections[0]; } +const MINT_COLLECTION_QUERY = gql` + query Mints($voter: String, $proposalId: String) { + mints(where: { minterAddress: $voter, proposal: $proposalId }, first: 1) { + id + } + } +`; + +type Mint = { + id: string; +}; + +export async function getMint(voter: string, proposalId: string) { + const { + data: { mints } + }: { data: { mints: Mint[] } } = await client.query({ + query: MINT_COLLECTION_QUERY, + variables: { + voter, + proposalId + } + }); + + return mints[0]; +} + export function numberizeProposalId(id: string) { return BigNumber.from(id.startsWith('0x') ? id : CID.parse(id).bytes).toString(); } diff --git a/test/unit/lib/nftClaimer/mint.test.ts b/test/unit/lib/nftClaimer/mint.test.ts index 90338dd..5bc5ab7 100644 --- a/test/unit/lib/nftClaimer/mint.test.ts +++ b/test/unit/lib/nftClaimer/mint.test.ts @@ -36,6 +36,10 @@ const mockMintingAllowed = jest.fn((space: any): boolean => { const mockHasVoted = jest.fn((address: string, proposal: string): boolean => { return true; }); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mockHasMinted = jest.fn((address: string, proposal: string): boolean => { + return false; +}); jest.mock('../../../../src/lib/nftClaimer/utils', () => { // Require the original module to not be mocked... const originalModule = jest.requireActual('../../../../src/lib/nftClaimer/utils'); @@ -46,7 +50,8 @@ jest.mock('../../../../src/lib/nftClaimer/utils', () => { getProposalContract: (id: string) => mockGetProposalContract(id), validateProposal: (id: any) => mockValidateProposal(id), mintingAllowed: (space: any) => mockMintingAllowed(space), - hasVoted: (address: string, proposal: string) => mockHasVoted(address, proposal) + hasVoted: (address: string, proposal: string) => mockHasVoted(address, proposal), + hasMinted: (address: string, proposal: string) => mockHasMinted(address, proposal) }; }); @@ -122,6 +127,13 @@ describe('nftClaimer', () => { }); }); + describe('when address has already minted', () => { + it('throws an error', () => { + mockHasMinted.mockReturnValueOnce(true); + return expect(async () => await payload(input)).rejects.toThrow(); + }); + }); + describe('when maxSupply has been reached', () => { it.todo('throws an error'); });