From 1970849b06a4ae929eed3092d6ee679759743180 Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Mon, 22 Jul 2024 17:58:28 -0300 Subject: [PATCH] feat: add mint authorization (#630) --- .../AuthorizationModal.spec.tsx | 61 +++++++++++++++++++ .../AuthorizationModal/AuthorizationModal.tsx | 1 + .../AuthorizationModal/utils.tsx | 6 +- .../withAuthorizedAction.tsx | 33 +++++++++- .../withAuthorizedAction.types.ts | 10 ++- src/modules/authorization/sagas.ts | 27 +++++++- src/modules/authorization/types.ts | 3 +- src/modules/authorization/utils.ts | 11 ++++ src/modules/translation/defaults/en.json | 4 ++ src/modules/translation/defaults/es.json | 6 +- src/modules/translation/defaults/fr.json | 4 ++ src/modules/translation/defaults/ja.json | 4 ++ src/modules/translation/defaults/ko.json | 4 ++ src/modules/translation/defaults/zh.json | 4 ++ 14 files changed, 172 insertions(+), 6 deletions(-) diff --git a/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.spec.tsx b/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.spec.tsx index 5bbe2c48..2afeb6d7 100644 --- a/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.spec.tsx +++ b/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.spec.tsx @@ -512,6 +512,67 @@ describe('when authorization type is ALLOWANCE', () => { }) }) +describe('when authorization type is MINT', () => { + let screen: RenderResult + describe('basic rendering', () => { + beforeEach(() => { + screen = renderAuthorizationModal({ + authorizationType: AuthorizationType.MINT + }) + }) + + it('should render two steps', () => { + expect(screen.getByTestId('multi-step').children.length).toBe(2) + }) + + it('should render authorization step', () => { + expect( + screen.getByRole('button', { + name: t('@dapps.authorization_modal.authorize_item.action') + }) + ).toBeInTheDocument() + }) + + it('should render confirm action step', () => { + expect( + screen.getByRole('button', { + name: t('@dapps.authorization_modal.confirm_transaction.action') + }) + ).toBeInTheDocument() + }) + + it('should show only first step enabled', () => { + expect( + screen.getByRole('button', { + name: t('@dapps.authorization_modal.authorize_item.action') + }) + ).toBeEnabled() + + expect( + screen.getByRole('button', { + name: t('@dapps.authorization_modal.confirm_transaction.action') + }) + ).toBeDisabled() + }) + }) + + describe('when clicking on authorize button', () => { + it('should call onGrant callback', async () => { + const onGrantMock = jest.fn() + const screen = renderAuthorizationModal({ + authorizationType: AuthorizationType.APPROVAL, + onGrant: onGrantMock + }) + await userEvent.click( + screen.getByRole('button', { + name: t('@dapps.authorization_modal.authorize_nft.action') + }) + ) + expect(onGrantMock).toHaveBeenCalled() + }) + }) +}) + describe('when clicking revoke authorization button', () => { let onRevokeMock: jest.Mock let trackMock: jest.Mock diff --git a/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.tsx b/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.tsx index 37f0fadc..d0c8b3b8 100644 --- a/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.tsx +++ b/src/containers/withAuthorizedAction/AuthorizationModal/AuthorizationModal.tsx @@ -208,6 +208,7 @@ export function AuthorizationModal({ confirmationStatus, confirmationError, authorizedContractLabel, + targetContractLabel, shouldReauthorize, currentStep, error, diff --git a/src/containers/withAuthorizedAction/AuthorizationModal/utils.tsx b/src/containers/withAuthorizedAction/AuthorizationModal/utils.tsx index dc240e3f..ff5c21b2 100644 --- a/src/containers/withAuthorizedAction/AuthorizationModal/utils.tsx +++ b/src/containers/withAuthorizedAction/AuthorizationModal/utils.tsx @@ -278,9 +278,13 @@ export function getSteps({ ] } + const title = + authorizationType === AuthorizationType.APPROVAL + ? 'authorize_nft.title' + : 'authorize_item.title' return [ { - title: getTranslation(translationKeys, 'authorize_nft.title', { + title: getTranslation(translationKeys, title, { contract: () => ( onAuthorized(false), + getConfirmationStatus, + getConfirmationError + }) } + setShowAuthorizationModal(true) } catch (error) { // TODO: handle error scenario diff --git a/src/containers/withAuthorizedAction/withAuthorizedAction.types.ts b/src/containers/withAuthorizedAction/withAuthorizedAction.types.ts index 977bba6f..d82f4964 100644 --- a/src/containers/withAuthorizedAction/withAuthorizedAction.types.ts +++ b/src/containers/withAuthorizedAction/withAuthorizedAction.types.ts @@ -36,7 +36,15 @@ type AllowanceOptions = AuthorizeBaseOptions & { requiredAllowanceInWei: string } -export type AuthorizeActionOptions = ApprovalOptions | AllowanceOptions +type MintOptions = AuthorizeBaseOptions & { + authorizationType: AuthorizationType.MINT + targetContractLabel?: string +} + +export type AuthorizeActionOptions = + | ApprovalOptions + | AllowanceOptions + | MintOptions type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] diff --git a/src/modules/authorization/sagas.ts b/src/modules/authorization/sagas.ts index 229be51d..e69fac20 100644 --- a/src/modules/authorization/sagas.ts +++ b/src/modules/authorization/sagas.ts @@ -9,6 +9,7 @@ import { getTransactionHashFromAction, waitForTx } from '../transaction/utils' import { getAnalytics } from '../analytics/utils' import { AuthorizationError, + getCollectionV2ContractInstance, getTokenAmountToApprove, hasAuthorization, hasAuthorizationAndEnoughAllowance, @@ -141,6 +142,24 @@ export function createAuthorizationSaga() { }) ) break + case AuthorizationType.MINT: + const collectionContract = getCollectionV2ContractInstance( + authorization.contractAddress, + multicallProviders[chainId] + ) + + promises.push( + collectionContract + .globalMinters(authorization.authorizedAddress) + .then((isMinter: boolean) => [ + authorization, + isMinter ? authorization : null + ]) + .catch((error: Error) => { + console.error(`Error fetching minters`, authorization, error) + return [authorization, null] + }) + ) } } @@ -270,7 +289,8 @@ export function createAuthorizationSaga() { } if ( - authorization.type === AuthorizationType.APPROVAL && + (authorization.type === AuthorizationType.APPROVAL || + authorization.type === AuthorizationType.MINT) && !hasAuthorization(authorizations, authorization) ) { throw new Error(AuthorizationError.GRANT_FAILED) @@ -310,6 +330,11 @@ export function createAuthorizationSaga() { return sendTransaction(contract, erc712 => erc712.setApprovalForAll(authorization.authorizedAddress, isApproved) ) + case AuthorizationType.MINT: + const isMinter = action === AuthorizationAction.GRANT + return sendTransaction(contract, collection => + collection.setMinters([authorization.authorizedAddress], [isMinter]) + ) } } } diff --git a/src/modules/authorization/types.ts b/src/modules/authorization/types.ts index eb912097..b7e7c128 100644 --- a/src/modules/authorization/types.ts +++ b/src/modules/authorization/types.ts @@ -3,7 +3,8 @@ import { ContractName } from 'decentraland-transactions' export enum AuthorizationType { ALLOWANCE = 'allowance', - APPROVAL = 'approval' + APPROVAL = 'approval', + MINT = 'mint' } export enum AuthorizationAction { diff --git a/src/modules/authorization/utils.ts b/src/modules/authorization/utils.ts index 0b60ba6f..81b44f5a 100644 --- a/src/modules/authorization/utils.ts +++ b/src/modules/authorization/utils.ts @@ -96,3 +96,14 @@ export function getERC721ContractInstance( provider ) } + +export function getCollectionV2ContractInstance( + contractAddress: string, + provider: Provider +) { + return new ethers.Contract( + contractAddress, + ['function globalMinters(address) view returns (bool)'], + provider + ) +} diff --git a/src/modules/translation/defaults/en.json b/src/modules/translation/defaults/en.json index ad1c70c8..019be1dd 100644 --- a/src/modules/translation/defaults/en.json +++ b/src/modules/translation/defaults/en.json @@ -244,6 +244,10 @@ "title": "First, authorize the contract to operate your NFTs on your behalf in Decentraland", "action": "Authorize" }, + "authorize_item": { + "title": "First, authorize the contract to mint items from your collection in Decentraland", + "action": "Authorize" + }, "confirm_transaction": { "title": "Confirm transaction to {action} the item", "action": "Confirm transaction" diff --git a/src/modules/translation/defaults/es.json b/src/modules/translation/defaults/es.json index 2157e97c..1edb5622 100644 --- a/src/modules/translation/defaults/es.json +++ b/src/modules/translation/defaults/es.json @@ -241,7 +241,11 @@ "message": "Recuerde establecer un límite igual al precio del artículo {price} MANA o superior." }, "authorize_nft": { - "title": "Primero, autorice el contrato para operar NFT en su nombre en Decentraland", + "title": "Primero, autorice el contrato para operar NFT en su nombre en Decentraland", + "action": "Autorizar" + }, + "authorize_item": { + "title": "Primero, autorice el contrato a mintear NFTs desde su colección en Decentraland", "action": "Autorizar" }, "confirm_transaction": { diff --git a/src/modules/translation/defaults/fr.json b/src/modules/translation/defaults/fr.json index f4c045f7..29c97318 100644 --- a/src/modules/translation/defaults/fr.json +++ b/src/modules/translation/defaults/fr.json @@ -166,6 +166,10 @@ "title": "Tout d'abord, autorisez le Contrat pour exploiter NFT en votre nom à Decentraland", "action": "Autoriser" }, + "authorize_item": { + "title": "Tout d'abord, autorisez le Contrat avec les articles de la menthe de votre collection en décentral et", + "action": "Authorize" + }, "confirm_transaction": { "title": "Confirmer la transaction à {action}l'articlem", "action": "Confirmer la transaction" diff --git a/src/modules/translation/defaults/ja.json b/src/modules/translation/defaults/ja.json index 07061116..ae742341 100644 --- a/src/modules/translation/defaults/ja.json +++ b/src/modules/translation/defaults/ja.json @@ -166,6 +166,10 @@ "title": "まず、<契約> 契約を承認して、Decentralandでお客様に代わってNFTを操作する", "action": "許可" }, + "authorize_item": { + "title": "まず、を認可します コレクションのアイテムをミントする契約 Decentralandで", + "action": "許可" + }, "confirm_transaction": { "title": "{action}アイテムへのトランザクションを確認します", "action": "トランザクションを確認します" diff --git a/src/modules/translation/defaults/ko.json b/src/modules/translation/defaults/ko.json index 60ce64cb..1960f266 100644 --- a/src/modules/translation/defaults/ko.json +++ b/src/modules/translation/defaults/ko.json @@ -166,6 +166,10 @@ "title": "먼저 승인하십시오 Decentraland에서 귀하를 대신하여 NFT를 운영하는 계약", "action": "승인" }, + "authorize_item": { + "title": "먼저 승인하십시오 컬렉션에서 Mint 품목에 계약을 체결하십시오 Decentraland에서", + "action": "승인" + }, "confirm_transaction": { "title": "{action} 항목에 대한 트랜잭션을 확인하십시오", "action": "거래 확인" diff --git a/src/modules/translation/defaults/zh.json b/src/modules/translation/defaults/zh.json index 97eec205..32a20e63 100644 --- a/src/modules/translation/defaults/zh.json +++ b/src/modules/translation/defaults/zh.json @@ -244,6 +244,10 @@ "title": "首先,授权 代表您在去居中的NFT的合同", "action": "授权" }, + "authorize_item": { + "title": "首先,授权 与您收藏的薄荷商品合同 在分散", + "action": "授权" + }, "confirm_transaction": { "title": "确认交易到 {action} 项目", "action": "确认交易"