diff --git a/src/antelope/config/index.ts b/src/antelope/config/index.ts index 4fc021669..32854cb15 100644 --- a/src/antelope/config/index.ts +++ b/src/antelope/config/index.ts @@ -4,7 +4,7 @@ import { getAntelope } from 'src/antelope'; import { AntelopeError, AntelopeErrorPayload } from 'src/antelope/types'; export class AntelopeConfig { - wrapError(description: string, error: unknown): AntelopeError { + transactionError(description: string, error: unknown): AntelopeError { if (error instanceof AntelopeError) { return error as AntelopeError; } diff --git a/src/antelope/stores/balances.ts b/src/antelope/stores/balances.ts index c93ff368a..6a401ed3b 100644 --- a/src/antelope/stores/balances.ts +++ b/src/antelope/stores/balances.ts @@ -244,7 +244,7 @@ export const useBalancesStore = defineStore(store_name, { this.processBalanceForToken(label, sys_token, balanceBn); } catch (error) { console.error(error); - throw getAntelope().config.wrapError('antelope.evm.error_update_system_balance_failed', error); + throw getAntelope().config.transactionError('antelope.evm.error_update_system_balance_failed', error); } }, shouldAddTokenBalance(label: string, balanceBn: BigNumber, token: TokenClass): boolean { @@ -322,7 +322,7 @@ export const useBalancesStore = defineStore(store_name, { .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); } } catch (error) { - const trxError = getAntelope().config.wrapError('antelope.evm.error_transfer_failed', error); + const trxError = getAntelope().config.transactionError('antelope.evm.error_transfer_failed', error); getAntelope().config.transactionErrorHandler(trxError, funcname); throw trxError; } finally { @@ -347,7 +347,7 @@ export const useBalancesStore = defineStore(store_name, { .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); } } catch (error) { - const trxError = getAntelope().config.wrapError('antelope.evm.error_wrap_failed', error); + const trxError = getAntelope().config.transactionError('antelope.evm.error_wrap_failed', error); getAntelope().config.transactionErrorHandler(trxError, funcname); throw trxError; } finally { @@ -371,7 +371,7 @@ export const useBalancesStore = defineStore(store_name, { .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); } } catch (error) { - const trxError = getAntelope().config.wrapError('antelope.evm.error_unwrap_failed', error); + const trxError = getAntelope().config.transactionError('antelope.evm.error_unwrap_failed', error); getAntelope().config.transactionErrorHandler(trxError, funcname); throw trxError; } finally { @@ -403,7 +403,7 @@ export const useBalancesStore = defineStore(store_name, { }); } catch (error) { console.error(error); - throw getAntelope().config.wrapError('antelope.evm.error_transfer_failed', error); + throw getAntelope().config.transactionError('antelope.evm.error_transfer_failed', error); } finally { useFeedbackStore().unsetLoading('transferNativeTokens'); } @@ -423,7 +423,7 @@ export const useBalancesStore = defineStore(store_name, { return result as EvmTransactionResponse | SendTransactionResult; } catch (error) { console.error(error); - throw getAntelope().config.wrapError('antelope.evm.error_transfer_failed', error); + throw getAntelope().config.transactionError('antelope.evm.error_transfer_failed', error); } finally { useFeedbackStore().unsetLoading('transferEVMTokens'); } diff --git a/src/antelope/stores/nfts.ts b/src/antelope/stores/nfts.ts index 3a8d2e75f..beef4a00c 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,35 @@ 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; + return await account.authenticator.transferNft(contractAddress, tokenId, type, from, to) + .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); + } catch (error) { + const trxError = getAntelope().config.transactionError('antelope.evm.error_transfer_nft', error); + getAntelope().config.transactionErrorHandler(trxError, funcname); + throw trxError; + } finally { + useFeedbackStore().unsetLoading(funcname); + } + }, }, }); diff --git a/src/antelope/stores/rex.ts b/src/antelope/stores/rex.ts index 184a80609..8edfe51f8 100644 --- a/src/antelope/stores/rex.ts +++ b/src/antelope/stores/rex.ts @@ -248,7 +248,7 @@ export const useRexStore = defineStore(store_name, { return await authenticator.stakeSystemTokens(amount) .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); } catch (error) { - const trxError = getAntelope().config.wrapError('antelope.evm.error_stakes_failed', error); + const trxError = getAntelope().config.transactionError('antelope.evm.error_stakes_failed', error); getAntelope().config.transactionErrorHandler(trxError, funcname); throw trxError; } finally { @@ -272,7 +272,7 @@ export const useRexStore = defineStore(store_name, { return await authenticator.unstakeSystemTokens(amount) .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); } catch (error) { - const trxError = getAntelope().config.wrapError('antelope.evm.error_unstakes_failed', error); + const trxError = getAntelope().config.transactionError('antelope.evm.error_unstakes_failed', error); getAntelope().config.transactionErrorHandler(trxError, funcname); throw trxError; } finally { @@ -296,7 +296,7 @@ export const useRexStore = defineStore(store_name, { return await authenticator.withdrawUnstakedTokens() .then(r => this.subscribeForTransactionReceipt(account, r as TransactionResponse)); } catch (error) { - const trxError = getAntelope().config.wrapError('antelope.evm.error_withdraw_failed', error); + const trxError = getAntelope().config.transactionError('antelope.evm.error_withdraw_failed', error); getAntelope().config.transactionErrorHandler(trxError, funcname); throw trxError; } finally { diff --git a/src/antelope/types/Filters.ts b/src/antelope/types/Filters.ts index 265ef4036..e1de2343b 100644 --- a/src/antelope/types/Filters.ts +++ b/src/antelope/types/Filters.ts @@ -40,4 +40,5 @@ export interface IndexerTransfersFilter { startBlock?: number; // first block to include in the query contract?: string; // filter by contract address includeAbi?: boolean; // indicate whether to include abi + tokenId?: number; // optional id for an NFT in a given collection } diff --git a/src/antelope/wallets/authenticators/EVMAuthenticator.ts b/src/antelope/wallets/authenticators/EVMAuthenticator.ts index 8b7374651..35d863fc8 100644 --- a/src/antelope/wallets/authenticators/EVMAuthenticator.ts +++ b/src/antelope/wallets/authenticators/EVMAuthenticator.ts @@ -2,13 +2,13 @@ import { SendTransactionResult, WriteContractResult } from '@wagmi/core'; import { BigNumber, ethers } from 'ethers'; -import { CURRENT_CONTEXT, getAntelope, useAccountStore } from 'src/antelope'; +import { CURRENT_CONTEXT, getAntelope, useAccountStore, useContractStore } from 'src/antelope'; import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; 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, EvmABI, EvmFunctionParam, EvmTransactionResponse, ExceptionError, TokenClass, addressString, erc20Abi, escrowAbiWithdraw, stlosAbiDeposit, stlosAbiWithdraw, wtlosAbiDeposit, wtlosAbiWithdraw } from 'src/antelope/types'; +import { AntelopeError, NftTokenInterface, ERC1155_TYPE, ERC721_TYPE, EvmABI, EvmABIEntry, EvmFunctionParam, EvmTransactionResponse, ExceptionError, TokenClass, addressString, erc20Abi, erc721Abi, escrowAbiWithdraw, stlosAbiDeposit, stlosAbiWithdraw, wtlosAbiDeposit, wtlosAbiWithdraw } from 'src/antelope/types'; export abstract class EVMAuthenticator { @@ -235,6 +235,31 @@ export abstract class EVMAuthenticator { } } + /** + * This method transfers NFTs between accounts + * @param contractAddress collection address + * @param tokenId id of the nft in collection + * @param type type of token, 721 or 1155 + * @param from address of sender + * @param to address of receiving account + * @param quantity optional value for 1155, default 1 for 721 + * @returns transaction response with the hash and a wait() method to wait confirmation + */ + async transferNft(contractAddress: string, tokenId: string, type: NftTokenInterface, from: addressString, to: addressString, quantity = 1): Promise { + this.trace('transferNft', contractAddress, tokenId, type, from, to); + const contract = await useContractStore().getContract(this.label, contractAddress); + if (contract) { + const transferAbi = erc721Abi.filter((abi:EvmABIEntry) => abi.name === 'safeTransferFrom'); + if (type === ERC721_TYPE){ + return this.signCustomTransaction(contractAddress, [transferAbi[0]], [from, to, tokenId]); + }else if (type === ERC1155_TYPE){ + return this.signCustomTransaction(contractAddress, [transferAbi[1]], [from, to, tokenId, quantity]); + } + } else { + throw new AntelopeError('antelope.balances.error_token_contract_not_found', { address: contractAddress }); + } + } + /** * This method creates a Transaction to wrap system tokens into ERC20 tokens * @param amount amount of system tokens to wrap diff --git a/src/antelope/wallets/authenticators/OreIdAuth.ts b/src/antelope/wallets/authenticators/OreIdAuth.ts index a355e199b..2281448f7 100644 --- a/src/antelope/wallets/authenticators/OreIdAuth.ts +++ b/src/antelope/wallets/authenticators/OreIdAuth.ts @@ -16,7 +16,6 @@ import { useChainStore } from 'src/antelope/stores/chain'; import { RpcEndpoint } from 'universal-authenticator-library'; import { TELOS_ANALYTICS_EVENT_IDS } from 'src/antelope/chains/chain-constants'; - const name = 'OreId'; export const OreIdAuthName = name; @@ -294,5 +293,4 @@ export class OreIdAuth extends EVMAuthenticator { return this.performOreIdTransaction(from, transactionBody); } - } diff --git a/src/components/evm/AppNav.vue b/src/components/evm/AppNav.vue index 83b096008..94586b61e 100644 --- a/src/components/evm/AppNav.vue +++ b/src/components/evm/AppNav.vue @@ -123,7 +123,7 @@ export default defineComponent({ // if the user has come from an external source, pressing back should go to the parent route const navigatedFromApp = sessionStorage.getItem('navigatedFromApp'); - if (navigatedFromApp) { + if (navigatedFromApp && this.$route.query.tab !== 'attributes') { // @TODO refactor to avoid explicit conditionals, required to nav back from details page but retain tab navigation history this.$router.go(-1); } else { const parent = this.$route.meta.parent as string; diff --git a/src/components/evm/AppPage.vue b/src/components/evm/AppPage.vue index 011b3df60..442c991ff 100644 --- a/src/components/evm/AppPage.vue +++ b/src/components/evm/AppPage.vue @@ -31,7 +31,7 @@ export default defineComponent({ } if (!this.tabs.includes(newValue.query.tab)) { - this.$router.replace({ query: { tab: this.tabs[0] } }); + this.$router.push({ path: this.$route.path, query: { ...this.$route.query, tab: this.tabs[0] } }); } } }, diff --git a/src/css/global/_global.scss b/src/css/global/_global.scss new file mode 100644 index 000000000..c349f143b --- /dev/null +++ b/src/css/global/_global.scss @@ -0,0 +1,8 @@ +.q-btn.wallet-btn { + @include text--header-5; + width: auto; + padding: 13px 24px; + &+& { + margin-left: 16px; + } +} diff --git a/src/css/global/global-index.scss b/src/css/global/global-index.scss index f3c4d502f..ac6eb15f1 100644 --- a/src/css/global/global-index.scss +++ b/src/css/global/global-index.scss @@ -6,3 +6,4 @@ @import 'z-index'; @import 'media-queries'; @import 'typography'; +@import 'global'; diff --git a/src/i18n/en-us/index.js b/src/i18n/en-us/index.js index 457f68a92..f70f80ad7 100644 --- a/src/i18n/en-us/index.js +++ b/src/i18n/en-us/index.js @@ -178,6 +178,11 @@ export default { empty_collection_message: 'Purchase your first collectible', empty_collection_link_text: 'here', collectibles_per_page: 'Collectibles per page', + // transfer + transfer: 'Transfer', + transfer_collectible: 'transfer collectible', + transfer_from: 'from', + transfer_on_telos: 'on Telos', }, evm_wrap: { wrap: 'Wrap', @@ -548,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/demo/SendPageErrors.vue b/src/pages/demo/SendPageErrors.vue index aa8d2c94f..309109d39 100644 --- a/src/pages/demo/SendPageErrors.vue +++ b/src/pages/demo/SendPageErrors.vue @@ -309,14 +309,6 @@ export default defineComponent({ 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/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 acced0288..ec338ebda 100644 --- a/src/pages/evm/wallet/SendPage.vue +++ b/src/pages/evm/wallet/SendPage.vue @@ -158,10 +158,6 @@ export default defineComponent({ return ethers.constants.Zero; } }, - 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)); }, @@ -406,14 +402,6 @@ export default defineComponent({