From 225f8261e9cde012090becd29b231b617f65d5ae Mon Sep 17 00:00:00 2001 From: donnyquixotic Date: Tue, 17 Oct 2023 16:15:34 -0500 Subject: [PATCH 01/20] feat(wip): add attr & transfer tab slots --- src/pages/evm/nfts/NftDetailsPage.vue | 168 +++++++++++++------------- 1 file changed, 87 insertions(+), 81 deletions(-) diff --git a/src/pages/evm/nfts/NftDetailsPage.vue b/src/pages/evm/nfts/NftDetailsPage.vue index cfefb03ba..ce92a4989 100644 --- a/src/pages/evm/nfts/NftDetailsPage.vue +++ b/src/pages/evm/nfts/NftDetailsPage.vue @@ -19,6 +19,8 @@ const { t: $t } = useI18n(); const nftStore = useNftsStore(); const chainStore = useChainStore(); +const tabs = ['attributes', 'transfer']; + const nft = ref(null); const loading = ref(true); @@ -71,7 +73,7 @@ const filteredAttributes = computed(() => @@ -334,4 +400,66 @@ const filteredAttributes = computed(() => height: 100px; } } + +.c-nft-transfer { + &__form-container { + animation: #{$anim-slide-in-left}; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } + + &__form { + width: 100%; + max-width: 530px; + } + + &__row { + gap: 16px; + &--1 { + margin-bottom: 24px; + } + + &--2 { + margin-bottom: 24px; + } + + &--3 { + margin-bottom: 24px; + } + } + + &__transfer-from{ + display: flex; + } + + &__transfer-text{ + font-size: 24px; + font-style: normal; + font-weight: 400; + + &--small{ + font-size: 16px; + } + + &--bold{ + font-weight: 600; + } + } +} + +// refactor as global +.q-btn.wallet-btn { + @include text--header-5; + &+& { + margin-left: 16px; + } +} + +// override UserInfo component styling +.o-text--header-4{ + font-size: 16px !important; +} diff --git a/src/pages/evm/nfts/NftInventoryPage.vue b/src/pages/evm/nfts/NftInventoryPage.vue index 475e60846..cd25942d1 100644 --- a/src/pages/evm/nfts/NftInventoryPage.vue +++ b/src/pages/evm/nfts/NftInventoryPage.vue @@ -254,7 +254,6 @@ watch(searchFilter, (filter) => { } }); - // methods function getCollectionUrl(address: string) { const explorer = (chainStore.currentChain.settings as EVMChainSettings).getExplorerUrl(); diff --git a/src/pages/evm/wallet/SendPage.vue b/src/pages/evm/wallet/SendPage.vue index 761a96f95..e45cea43c 100644 --- a/src/pages/evm/wallet/SendPage.vue +++ b/src/pages/evm/wallet/SendPage.vue @@ -158,10 +158,10 @@ export default defineComponent({ return ethers.constants.Zero; } }, - isAddressValid(): boolean { - const regex = /^0x[a-fA-F0-9]{40}$/; - return regex.test(this.address); - }, + // isAddressValid(): boolean { + // const regex = /^0x[a-fA-F0-9]{40}$/; + // return regex.test(this.address); + // }, isFormValid(): boolean { return this.addressIsValid && !(this.amount.isZero() || this.amount.isNegative() || this.amount.gt(this.availableInTokensBn)); }, From 1d066078b85254f9dfa92eff615cde001e089ed4 Mon Sep 17 00:00:00 2001 From: donnyquixotic Date: Wed, 18 Oct 2023 16:06:24 -0500 Subject: [PATCH 03/20] feat: transfer NFT (721) --- src/antelope/config/index.ts | 1 + src/antelope/stores/nfts.ts | 38 +++++++++++++++++-- .../authenticators/EVMAuthenticator.ts | 3 +- .../authenticators/InjectedProviderAuth.ts | 17 +++++++++ .../wallets/authenticators/OreIdAuth.ts | 7 ++++ .../authenticators/WalletConnectAuth.ts | 8 ++++ src/i18n/en-us/index.js | 1 + src/pages/evm/nfts/NftDetailsPage.vue | 26 +++++++------ 8 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/antelope/config/index.ts b/src/antelope/config/index.ts index d7d5e3d2a..d58e4adbb 100644 --- a/src/antelope/config/index.ts +++ b/src/antelope/config/index.ts @@ -4,6 +4,7 @@ import { getAntelope } from 'src/antelope'; import { AntelopeError, AntelopeErrorPayload } from 'src/antelope/types'; export class AntelopeConfig { + // @TODO rename this method, it's used for token and NFT transfers as well wrapError(description: string, error: unknown): AntelopeError { if (error instanceof AntelopeError) { return error as AntelopeError; diff --git a/src/antelope/stores/nfts.ts b/src/antelope/stores/nfts.ts index 3a8d2e75f..2eb6f5e28 100644 --- a/src/antelope/stores/nfts.ts +++ b/src/antelope/stores/nfts.ts @@ -1,20 +1,21 @@ /** * NFTs: This store is responsible for all functionality pertaining to NFTs, - * such as fetching them for a given account or getting information on a particular NFT + * such as fetching them for a given account, getting information on a particular NFT, or transferring NFTs */ import { defineStore } from 'pinia'; -import { Label, Network, Address, IndexerTransactionsFilter, NFTClass, NftTokenInterface } from 'src/antelope/types'; +import { Label, Network, Address, IndexerTransactionsFilter, NFTClass, NftTokenInterface, TransactionResponse, addressString } from 'src/antelope/types'; import { useFeedbackStore, getAntelope, useChainStore, useEVMStore, CURRENT_CONTEXT } from 'src/antelope'; import { createTraceFunction, isTracingAll } from 'src/antelope/stores/feedback'; import { toRaw } from 'vue'; -import { AccountModel } from 'src/antelope/stores/account'; +import { AccountModel, EvmAccountModel, useAccountStore } from 'src/antelope/stores/account'; import NativeChainSettings from 'src/antelope/chains/NativeChainSettings'; import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; import { errorToString } from 'src/antelope/config'; import { truncateAddress } from 'src/antelope/stores/utils/text-utils'; +import { subscribeForTransactionReceipt } from 'src/antelope/stores/utils/trx-utils'; export interface NFTsInventory { owner: Address; @@ -126,6 +127,7 @@ export const useNftsStore = defineStore(store_name, { }, }); }, + async updateNFTsForAccount(label: string, account: AccountModel | null) { this.trace('updateNFTsForAccount', label, account); if (!account?.account) { @@ -280,6 +282,36 @@ export const useNftsStore = defineStore(store_name, { limit: 10000, }); }, + + async subscribeForTransactionReceipt(account: EvmAccountModel, response: TransactionResponse): Promise { + this.trace('subscribeForTransactionReceipt', account.account, response.hash); + return subscribeForTransactionReceipt(account, response).then(({ newResponse, receipt }) => { + newResponse.wait().then(() => { + this.trace('subscribeForTransactionReceipt', newResponse.hash, 'receipt:', receipt.status, receipt); + this.updateNFTsForAccount(CURRENT_CONTEXT, account); + }); + return newResponse; + }); + }, + + async transferNft(label: Label, contractAddress: string, tokenId: string, type: NftTokenInterface, from: addressString, to: addressString): Promise { + const funcname = 'transferNft'; + this.trace(funcname, label, contractAddress, tokenId, type); + + try { + useFeedbackStore().setLoading(funcname); + const account = useAccountStore().loggedAccount as EvmAccountModel; + const authenticator = useAccountStore().getEVMAuthenticator(label); + return await authenticator.transferNft(contractAddress, tokenId, type, from, to) + .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); + } catch (error) { + const trxError = getAntelope().config.wrapError('antelope.evm.error_transfer_nft', error); + getAntelope().config.transactionErrorHandler(trxError, funcname); + throw trxError; + } finally { + useFeedbackStore().unsetLoading(funcname); + } + }, }, }); diff --git a/src/antelope/wallets/authenticators/EVMAuthenticator.ts b/src/antelope/wallets/authenticators/EVMAuthenticator.ts index 18cb2093d..fe657438e 100644 --- a/src/antelope/wallets/authenticators/EVMAuthenticator.ts +++ b/src/antelope/wallets/authenticators/EVMAuthenticator.ts @@ -8,7 +8,7 @@ import { useChainStore } from 'src/antelope/stores/chain'; import { useEVMStore } from 'src/antelope/stores/evm'; import { createTraceFunction, isTracingAll, useFeedbackStore } from 'src/antelope/stores/feedback'; import { usePlatformStore } from 'src/antelope/stores/platform'; -import { AntelopeError, EvmTransactionResponse, ExceptionError, TokenClass, addressString } from 'src/antelope/types'; +import { AntelopeError, EvmTransactionResponse, ExceptionError, NftTokenInterface, TokenClass, addressString } from 'src/antelope/types'; export abstract class EVMAuthenticator { @@ -36,6 +36,7 @@ export abstract class EVMAuthenticator { abstract externalProvider(): Promise; abstract web3Provider(): Promise; abstract getSigner(): Promise; + abstract transferNft(contract: string, tokenId: string, type: NftTokenInterface, from: addressString, to: addressString): Promise; // to easily clone the authenticator abstract newInstance(label: string): EVMAuthenticator; diff --git a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts index e05df54e0..dea8993e1 100644 --- a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts +++ b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts @@ -5,9 +5,11 @@ import { BehaviorSubject, filter, map } from 'rxjs'; import { useContractStore, useEVMStore, useFeedbackStore, useRexStore } from 'src/antelope'; import { AntelopeError, + ERC1155_TYPE, ERC20_TYPE, EthereumProvider, EvmTransactionResponse, + NftTokenInterface, TokenClass, addressString, wtlosAbiDeposit, @@ -241,6 +243,21 @@ export abstract class InjectedProviderAuth extends EVMAuthenticator { } } + async transferNft(contractAddress: string, tokenId: string, type: NftTokenInterface, from: addressString, to: addressString): Promise { + this.trace('transferNft', contractAddress, tokenId, type, to); + if (type === ERC1155_TYPE) { + console.log(ERC1155_TYPE); + } else { + const contract = await useContractStore().getContract(this.label, contractAddress); + if (contract) { + const contractInstance = await contract.getContractInstance(); + return contractInstance['safeTransferFrom(address,address,uint256)'](from, to, tokenId); + } else { + throw new AntelopeError('antelope.balances.error_token_contract_not_found', { address: contractAddress }); + } + } + } + prepareTokenForTransfer(token: TokenClass | null, amount: ethers.BigNumber, to: string): Promise { this.trace('prepareTokenForTransfer', [token], amount, to); return new Promise((resolve) => { diff --git a/src/antelope/wallets/authenticators/OreIdAuth.ts b/src/antelope/wallets/authenticators/OreIdAuth.ts index b3b0d0332..ea9063e38 100644 --- a/src/antelope/wallets/authenticators/OreIdAuth.ts +++ b/src/antelope/wallets/authenticators/OreIdAuth.ts @@ -2,6 +2,7 @@ import { AuthProvider, ChainNetwork, OreId, OreIdOptions, JSONObject, UserChainA import { BigNumber, ethers } from 'ethers'; import { WebPopup } from 'oreid-webpopup'; import { + NftTokenInterface, erc20Abi, escrowAbiWithdraw, stlosAbiDeposit, @@ -246,6 +247,12 @@ export class OreIdAuth extends EVMAuthenticator { return this.performOreIdTransaction(from, transactionBody); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async transferNft(contractAddress: string, tokenId: string, type: NftTokenInterface, from: addressString, to: addressString): Promise { + // @TODO + return; + } + async prepareTokenForTransfer(token: TokenClass | null, amount: ethers.BigNumber, to: string): Promise { this.trace('prepareTokenForTransfer', [token], amount, to); } diff --git a/src/antelope/wallets/authenticators/WalletConnectAuth.ts b/src/antelope/wallets/authenticators/WalletConnectAuth.ts index 26866ebef..79f199f4a 100644 --- a/src/antelope/wallets/authenticators/WalletConnectAuth.ts +++ b/src/antelope/wallets/authenticators/WalletConnectAuth.ts @@ -25,6 +25,8 @@ import { usePlatformStore } from 'src/antelope/stores/platform'; import { AntelopeError, EvmABI, + EvmTransactionResponse, + NftTokenInterface, TokenClass, addressString, escrowAbiWithdraw, @@ -258,6 +260,12 @@ export class WalletConnectAuth extends EVMAuthenticator { } } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async transferNft(contractAddress: string, tokenId: string, type: NftTokenInterface, from: addressString, to: addressString): Promise { + // @TODO + return; + } + readyForTransfer(): boolean { return !!this.sendConfig; } diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index e9b8da5f5..f70f80ad7 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -553,6 +553,7 @@ export default { error_unstakes_failed: 'An unknown error occurred when unstaking tokens', error_withdraw_failed: 'An unknown error occurred when withdrawing tokens', error_fetching_token_price: 'An unknown error occurred when fetching token price data', + error_transfer_nft: 'An error occured while transferring collectible', }, history: { error_fetching_transactions: 'Unexpected error fetching transactions. Please refresh the page to try again.', diff --git a/src/pages/evm/nfts/NftDetailsPage.vue b/src/pages/evm/nfts/NftDetailsPage.vue index b79b48913..ce683cc8e 100644 --- a/src/pages/evm/nfts/NftDetailsPage.vue +++ b/src/pages/evm/nfts/NftDetailsPage.vue @@ -23,13 +23,16 @@ const chainStore = useChainStore(); const accountStore = useAccountStore(); const tabs = ['attributes', 'transfer']; -const address = ''; +const explorerUrl = (chainStore.currentChain.settings as EVMChainSettings).getExplorerUrl(); +let addressIsValid = false; const nft = ref(null); const loading = ref(true); +const address = ref(''); const contractAddress = route.query.contract as string; const nftId = route.query.id as string; +let nftType: ERC1155_TYPE | ERC721_TYPE | null = null; onBeforeMount(async () => { if (contractAddress && nftId) { @@ -38,18 +41,15 @@ onBeforeMount(async () => { if (erc721Details) { nft.value = erc721Details; + nftType = ERC721_TYPE; } else if (erc1155Details) { nft.value = erc1155Details; + nftType = ERC1155_TYPE; } loading.value = false; } }); -// data -const explorerUrl = (chainStore.currentChain.settings as EVMChainSettings).getExplorerUrl(); -const addressIsValid = false; - -// computed const contractAddressIsValid = computed( () => isValidAddressFormat(contractAddress), ); @@ -78,7 +78,9 @@ const loggedAccount = computed(() => accountStore.loggedEvmAccount, ); -const startTransfer = () => console.log('test'); +async function startTransfer(){ + await nftStore.transferNft(CURRENT_CONTEXT, contractAddress, nftId, nftType, loggedAccount.value.address, address.value); +} @@ -442,6 +444,10 @@ const startTransfer = () => console.log('test'); &--small{ font-size: 16px; + // override UserInfo component styling + .o-text--header-4{ + font-size: 16px !important; + } } &--bold{ @@ -450,7 +456,7 @@ const startTransfer = () => console.log('test'); } } -// refactor as global +// refactor as global used in several places .q-btn.wallet-btn { @include text--header-5; &+& { @@ -458,8 +464,4 @@ const startTransfer = () => console.log('test'); } } -// override UserInfo component styling -.o-text--header-4{ - font-size: 16px !important; -} From 748cd97a9dab0a79928e3fd3f92f6755bfd7d373 Mon Sep 17 00:00:00 2001 From: donnyquixotic Date: Wed, 18 Oct 2023 16:39:49 -0500 Subject: [PATCH 04/20] feat: transfer NFT (1155), injected --- .../authenticators/InjectedProviderAuth.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts index dea8993e1..ddb7d48b6 100644 --- a/src/antelope/wallets/authenticators/InjectedProviderAuth.ts +++ b/src/antelope/wallets/authenticators/InjectedProviderAuth.ts @@ -7,6 +7,7 @@ import { AntelopeError, ERC1155_TYPE, ERC20_TYPE, + ERC721_TYPE, EthereumProvider, EvmTransactionResponse, NftTokenInterface, @@ -244,17 +245,17 @@ export abstract class InjectedProviderAuth extends EVMAuthenticator { } async transferNft(contractAddress: string, tokenId: string, type: NftTokenInterface, from: addressString, to: addressString): Promise { - this.trace('transferNft', contractAddress, tokenId, type, to); - if (type === ERC1155_TYPE) { - console.log(ERC1155_TYPE); - } else { - const contract = await useContractStore().getContract(this.label, contractAddress); - if (contract) { - const contractInstance = await contract.getContractInstance(); + this.trace('transferNft', contractAddress, tokenId, type, from, to); + const contract = await useContractStore().getContract(this.label, contractAddress); + if (contract) { + const contractInstance = await contract.getContractInstance(); + if (type === ERC721_TYPE){ return contractInstance['safeTransferFrom(address,address,uint256)'](from, to, tokenId); - } else { - throw new AntelopeError('antelope.balances.error_token_contract_not_found', { address: contractAddress }); + }else if (type === ERC1155_TYPE){ + return contractInstance['safeTransferFrom(address,address,uint256,bytes)'](from, to, tokenId, 1); } + } else { + throw new AntelopeError('antelope.balances.error_token_contract_not_found', { address: contractAddress }); } } From 3dc9fe156d04051c4d91953c5635921e09c03373 Mon Sep 17 00:00:00 2001 From: donnyquixotic Date: Mon, 23 Oct 2023 16:54:59 -0500 Subject: [PATCH 05/20] feat: success and pending notifications for nft transfer --- src/pages/evm/nfts/NftDetailsPage.vue | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pages/evm/nfts/NftDetailsPage.vue b/src/pages/evm/nfts/NftDetailsPage.vue index ce683cc8e..adc155e95 100644 --- a/src/pages/evm/nfts/NftDetailsPage.vue +++ b/src/pages/evm/nfts/NftDetailsPage.vue @@ -9,7 +9,7 @@ import NftDetailsCard from 'pages/evm/nfts/NftDetailsCard.vue'; import ExternalLink from 'components/ExternalLink.vue'; import AddressInput from 'components/evm/inputs/AddressInput.vue'; import UserInfo from 'components/evm/UserInfo.vue'; -import { CURRENT_CONTEXT, useChainStore, useAccountStore } from 'src/antelope'; +import { CURRENT_CONTEXT, useChainStore, useAccountStore, getAntelope } from 'src/antelope'; import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; import NumberedList from 'components/NumberedList.vue'; import { isValidAddressFormat } from 'src/antelope/stores/utils'; @@ -18,6 +18,7 @@ import { useI18n } from 'vue-i18n'; const route = useRoute(); const { t: $t } = useI18n(); +const ant = getAntelope(); const nftStore = useNftsStore(); const chainStore = useChainStore(); const accountStore = useAccountStore(); @@ -79,7 +80,20 @@ const loggedAccount = computed(() => ); async function startTransfer(){ - await nftStore.transferNft(CURRENT_CONTEXT, contractAddress, nftId, nftType, loggedAccount.value.address, address.value); + const nameString = `${nft.value.contractPrettyName || nft.value.contractAddress} #${nft.value.id}`; + const dismiss = ant.config.notifyNeutralMessageHandler( + $t('notification.neutral_message_sending', { quantity: nameString, address: address.value }), + ); + try{ + const trx = await nftStore.transferNft(CURRENT_CONTEXT, contractAddress, nftId, nftType, loggedAccount.value.address, address.value); + const chain_settings = ant.stores.chain.loggedEvmChain?.settings; + dismiss(); + ant.config.notifySuccessfulTrxHandler( + `${chain_settings.getExplorerUrl()}/tx/${trx.hash}`, + ); + }catch(e){ + console.error(e); // tx error notification handled in store + } } From dddd103956525af1f786d51f232508071a69e399 Mon Sep 17 00:00:00 2001 From: donnyquixotic Date: Mon, 23 Oct 2023 17:10:18 -0500 Subject: [PATCH 06/20] feat: return to attributes tab on successful transfer --- src/pages/evm/nfts/NftDetailsPage.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/evm/nfts/NftDetailsPage.vue b/src/pages/evm/nfts/NftDetailsPage.vue index adc155e95..2a6096e9b 100644 --- a/src/pages/evm/nfts/NftDetailsPage.vue +++ b/src/pages/evm/nfts/NftDetailsPage.vue @@ -1,7 +1,7 @@ diff --git a/src/pages/evm/wallet/ReceivePage.vue b/src/pages/evm/wallet/ReceivePage.vue index 11d3e2614..22335880b 100644 --- a/src/pages/evm/wallet/ReceivePage.vue +++ b/src/pages/evm/wallet/ReceivePage.vue @@ -79,15 +79,6 @@ export default defineComponent({ diff --git a/src/pages/evm/wallet/SendPage.vue b/src/pages/evm/wallet/SendPage.vue index a3c662992..d7bc609e5 100644 --- a/src/pages/evm/wallet/SendPage.vue +++ b/src/pages/evm/wallet/SendPage.vue @@ -402,14 +402,6 @@ export default defineComponent({