Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#606 | Edit and revoke allowances #717

Merged
merged 40 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e556a09
Merge branch 'epic-allowances' of github.com:telosnetwork/telos-walle…
ezra-sg Dec 8, 2023
bb6fd5c
add single erc20 allowance update capability
ezra-sg Dec 8, 2023
924db8d
add single erc721 approval capability
ezra-sg Dec 8, 2023
560f995
WIP add collection set approval
ezra-sg Dec 11, 2023
91c4acd
finish adding nft collection allowance edit
ezra-sg Dec 11, 2023
6b9ec4c
cleanup, fix pagination
ezra-sg Dec 12, 2023
748a4f7
integrate revoke button with table rows
ezra-sg Dec 12, 2023
e4dd0b8
WIP add batch revoke
ezra-sg Dec 12, 2023
a9a4332
Merge branch 'develop' of github.com:telosnetwork/telos-wallet into 6…
ezra-sg Dec 12, 2023
74a5003
fix allowance response handling when there are no erc721 results
ezra-sg Dec 12, 2023
f8ab43e
UX enhancements
ezra-sg Dec 13, 2023
4779a9e
cleanup
ezra-sg Dec 14, 2023
d727de4
Merge branch '605-revoke-allowances-ui' into 606-revoke-allowances-ap…
ezra-sg Dec 14, 2023
794c6f6
fix single erc721 support
ezra-sg Dec 14, 2023
be2519a
type safety
ezra-sg Dec 14, 2023
e3e0b71
fix single erc721 handling
ezra-sg Dec 14, 2023
aa74acd
i18n
ezra-sg Dec 14, 2023
dd18174
fix scenario causing an nft collection to fail to load
ezra-sg Dec 15, 2023
6e97652
fix accounts being stored in contract store
ezra-sg Dec 15, 2023
45f86a6
fix logged out error, cleanup
ezra-sg Dec 15, 2023
0a885cd
cleanup
ezra-sg Dec 15, 2023
26f9939
fix erc721 owner not being set
ezra-sg Dec 15, 2023
d302fea
fix accounts being stored as contracts #2
ezra-sg Dec 15, 2023
07cacb1
improve tooltip text for erc721 single rows
ezra-sg Dec 15, 2023
73c5442
error handling
ezra-sg Dec 15, 2023
0dacd13
Merge branch 'epic-allowances' of github.com:telosnetwork/telos-walle…
ezra-sg Dec 15, 2023
d0b5ee8
Merge branch 'develop' of github.com:telosnetwork/telos-wallet into 6…
ezra-sg Dec 15, 2023
7afd686
Merge branch 'epic-allowances' into 606-revoke-allowances-api-integra…
ezra-sg Dec 15, 2023
4b0cacb
cleanup
ezra-sg Dec 18, 2023
41ee317
add successful tx notification, improve modal text, cleanup
ezra-sg Dec 18, 2023
d5b8525
remove cancelled allowances toggle temporarily
ezra-sg Dec 18, 2023
4603143
WIP support walletconnect - update erc20 allowance
ezra-sg Dec 18, 2023
c66268e
WIP support walletconnect - update single erc721 allowance
ezra-sg Dec 18, 2023
494ec6f
support walletconnect - update nft collection allowance
ezra-sg Dec 19, 2023
aacee16
add missed file
ezra-sg Dec 19, 2023
7883930
improve batch revoke copy
ezra-sg Dec 20, 2023
af5bb77
fix potential case mismatch
ezra-sg Dec 21, 2023
2f85045
Update src/i18n/en-us/index.js
donnyquixotic Dec 26, 2023
458c2bd
Update src/i18n/en-us/index.js
donnyquixotic Dec 26, 2023
d33dea7
fix: linter, escape quote
donnyquixotic Dec 26, 2023
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
4 changes: 4 additions & 0 deletions src/antelope/chains/EVMChainSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export default abstract class EVMChainSettings implements ChainSettings {
imageCache: nftResponse.imageCache,
tokenUri: nftResponse.tokenUri,
supply: nftResponse.supply,
owner: nftResponse.owner,
}));

// we fix the supportedInterfaces property if it is undefined in the response but present in the request
Expand Down Expand Up @@ -728,6 +729,7 @@ export default abstract class EVMChainSettings implements ChainSettings {
const params = {
...filter,
type: 'erc20',
all: true,
};
const response = await this.indexer.get(`v1/account/${account}/approvals`, { params });
return response.data as IndexerAllowanceResponseErc20;
Expand All @@ -737,6 +739,7 @@ export default abstract class EVMChainSettings implements ChainSettings {
const params = {
...filter,
type: 'erc721',
all: true,
};
const response = await this.indexer.get(`v1/account/${account}/approvals`, { params });
return response.data as IndexerAllowanceResponseErc721;
Expand All @@ -746,6 +749,7 @@ export default abstract class EVMChainSettings implements ChainSettings {
const params = {
...filter,
type: 'erc1155',
all: true,
};
const response = await this.indexer.get(`v1/account/${account}/approvals`, { params });
return response.data as IndexerAllowanceResponseErc1155;
Expand Down
2 changes: 2 additions & 0 deletions src/antelope/chains/chain-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export const TELOS_ANALYTICS_EVENT_IDS = {
loginFailedWalletConnect: '9V4IV1BV',
loginSuccessfulWalletConnect: '2EG2OR3H',
};

export const ZERO_ADDRESS = '0x'.concat('0'.repeat(40));
294 changes: 271 additions & 23 deletions src/antelope/stores/allowances.ts

Large diffs are not rendered by default.

56 changes: 49 additions & 7 deletions src/antelope/stores/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export interface ContractStoreState {
processing: Record<string, Promise<EvmContract | null>>
},
}
// addresses which have been checked and are known not to be contract addresses
__accounts: {
[network: string]: string[],
},
}

const store_name = 'contract';
Expand Down Expand Up @@ -187,6 +191,13 @@ export const useContractStore = defineStore(store_name, {
return this.__contracts[network].cached[addressLower];
}

const isContract = await this.addressIsContract(network, address);

if (!isContract) {
// address is an account, not a contract
return null;
}

// if we have the metadata, we can create the contract and return it
if (typeof this.__contracts[network].metadata[addressLower] !== 'undefined') {
const metadata = this.__contracts[network].metadata[addressLower] as EvmContractFactoryData;
Expand Down Expand Up @@ -258,7 +269,7 @@ export const useContractStore = defineStore(store_name, {

if (metadata && creationInfo) {
this.trace('fetchContractUsingHyperion', 'returning verified contract', address, metadata, creationInfo);
return resolve(this.createAndStoreVerifiedContract(label, addressLower, metadata, creationInfo, suspectedToken));
return resolve(await this.createAndStoreVerifiedContract(label, addressLower, metadata, creationInfo, suspectedToken));
}

const tokenContract = await this.createAndStoreContractFromTokenList(label, address, suspectedToken, creationInfo);
Expand All @@ -275,7 +286,7 @@ export const useContractStore = defineStore(store_name, {

if (creationInfo) {
this.trace('fetchContractUsingHyperion', 'returning empty contract', address, creationInfo);
return resolve(this.createAndStoreEmptyContract(label, addressLower, creationInfo));
return resolve(await this.createAndStoreEmptyContract(label, addressLower, creationInfo));
} else {
// We mark this address as not existing so we don't query it again
this.trace('fetchContractUsingHyperion', 'returning null', address);
Expand Down Expand Up @@ -417,10 +428,10 @@ export const useContractStore = defineStore(store_name, {
metadata: EvmContractMetadata,
creationInfo: EvmContractCreationInfo,
suspectedType: string,
): Promise<EvmContract> {
): Promise<EvmContract | null> {
this.trace('createAndStoreVerifiedContract', label, address, [metadata], [creationInfo], suspectedType);
const token = await this.getToken(label, address, suspectedType) ?? undefined;
return this.createAndStoreContract(label, address, {
return await this.createAndStoreContract(label, address, {
name: Object.values(metadata.settings?.compilationTarget ?? {})[0],
address,
abi: metadata.output?.abi,
Expand All @@ -442,9 +453,9 @@ export const useContractStore = defineStore(store_name, {
label: string,
address:string,
creationInfo: EvmContractCreationInfo | null,
): Promise<EvmContract> {
): Promise<EvmContract | null> {
this.trace('createAndStoreEmptyContract', label, address, [creationInfo]);
return this.createAndStoreContract(label, address, {
return await this.createAndStoreContract(label, address, {
name: `0x${address.slice(0, 16)}...`,
address,
creationInfo,
Expand Down Expand Up @@ -513,7 +524,7 @@ export const useContractStore = defineStore(store_name, {
},

// commits -----
createAndStoreContract(label: string, address: string, metadata: EvmContractFactoryData): EvmContract {
async createAndStoreContract(label: string, address: string, metadata: EvmContractFactoryData): Promise<EvmContract | null> {
const network = useChainStore().getChain(label).settings.getNetwork();
this.trace('createAndStoreContract', label, network, address, [metadata]);
if (!address) {
Expand All @@ -522,6 +533,14 @@ export const useContractStore = defineStore(store_name, {
if (!label) {
throw new AntelopeError('antelope.contracts.error_label_required');
}

const isContract = await this.addressIsContract(network, address);

if (!isContract) {
// address is an account, not a contract
return null;
}

const index = address.toString().toLowerCase();

// If:
Expand Down Expand Up @@ -557,10 +576,33 @@ export const useContractStore = defineStore(store_name, {
const index = address.toString().toLowerCase();
this.__contracts[network].cached[index] = null;
},

async addressIsContract(network: string, address: string) {
const addressLower = address.toLowerCase();
if (!this.__accounts[network]) {
this.__accounts[network] = [];
}

if (this.__accounts[network].includes(addressLower)) {
return false;
}

const provider = await getAntelope().wallets.getWeb3Provider();
const code = await provider.getCode(address);

const isContract = code !== '0x';

if (!isContract && !this.__accounts[network].includes(addressLower)) {
this.__accounts[network].push(addressLower);
}

return isContract;
},
},
});

const contractInitialState: ContractStoreState = {
__contracts: {},
__factory: new EvmContractFactory(),
__accounts: {},
};
33 changes: 21 additions & 12 deletions src/antelope/stores/nfts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface NFTsCollection {
contract: Address;
list: Collectible[];
loading: boolean;

// this is to prevent the scenario where we fetch a single NFT from a collection, add it to a contract's `list`
// and then in future checks we assume that the entire collection has been fetched (as we have at least one item in the list)
entireCollectionFetched: boolean;
}

export interface UserNftFilter {
Expand Down Expand Up @@ -220,14 +224,15 @@ export const useNftsStore = defineStore(store_name, {

// If we already have a contract for that network and contract, we search for the NFT in that list first
this.__contracts[network] = this.__contracts[network] || {};
if (this.__contracts[network][contractLower]) {
if (this.__contracts[network][contractLower].loading) {
let waitCount = 0;
while (this.__contracts[network][contractLower].loading && waitCount++ < 600) {
await new Promise(resolve => setTimeout(resolve, 100));
}

if (this.__contracts[network]?.[contractLower]?.loading) {
let waitCount = 0;
while (this.__contracts[network][contractLower].loading && waitCount++ < 600) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}

if (this.__contracts[network]?.[contractLower]?.entireCollectionFetched) {
const nft = this.__contracts[network][contractLower].list.find(
nft => nft.contractAddress.toLowerCase() === contract.toLowerCase() && nft.id === tokenId,
);
Expand All @@ -239,6 +244,7 @@ export const useNftsStore = defineStore(store_name, {
contract: contractLower,
list: [],
loading: false,
entireCollectionFetched: false,
};
}

Expand Down Expand Up @@ -289,13 +295,14 @@ export const useNftsStore = defineStore(store_name, {
const chain = useChainStore().getChain(label);
const network = chain.settings.getNetwork();

if (this.__contracts[network] && this.__contracts[network][contractLower]) {
if (this.__contracts[network][contractLower].loading) {
let waitCount = 0;
while (this.__contracts[network][contractLower].loading && waitCount++ < 600) {
await new Promise(resolve => setTimeout(resolve, 100));
}
if (this.__contracts[network]?.[contractLower]?.loading) {
let waitCount = 0;
while (this.__contracts[network][contractLower].loading && waitCount++ < 600) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}

if (this.__contracts[network]?.[contractLower]?.entireCollectionFetched) {
return Promise.resolve(this.__contracts[network][contractLower].list);
}

Expand All @@ -308,6 +315,7 @@ export const useNftsStore = defineStore(store_name, {
contract,
list: [],
loading: true,
entireCollectionFetched: false,
};
}

Expand All @@ -317,6 +325,7 @@ export const useNftsStore = defineStore(store_name, {
try {
const nfts = await chain.settings.getNftsForCollection(contract, { limit: 10000 });
this.__contracts[network][contractLower].list = nfts;
this.__contracts[network][contractLower].entireCollectionFetched = true;

return nfts;
} catch {
Expand Down
27 changes: 26 additions & 1 deletion src/antelope/stores/utils/abi/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,29 @@ export const erc20Abi = [
'name': 'Approval',
'type': 'event',
},
] as EvmABI;
] as EvmABI;

export const erc20AbiApprove = [{
'inputs': [
{
'internalType': 'address',
'name': 'spender',
'type': 'address',
},
{
'internalType': 'uint256',
'name': 'amount',
'type': 'uint256',
},
],
'name': 'approve',
'outputs': [
{
'internalType': 'bool',
'name': '',
'type': 'bool',
},
],
'stateMutability': 'nonpayable',
'type': 'function',
}] as EvmABI;
13 changes: 13 additions & 0 deletions src/antelope/stores/utils/abi/erc721.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,16 @@ export const erc721Abi = [{
'stateMutability': 'nonpayable',
'type': 'function',
}] as EvmABI;

export const erc721ApproveAbi = [{
'inputs': [{ 'internalType': 'address', 'name': 'to', 'type': 'address' }, {
'internalType': 'uint256',
'name': 'tokenId',
'type': 'uint256',
}],
'name': 'approve',
'outputs': [],
'stateMutability': 'nonpayable',
'type': 'function',
}] as unknown as EvmABI;

13 changes: 13 additions & 0 deletions src/antelope/stores/utils/abi/setApprovalForAllAbi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EvmABI } from '.';

export const setApprovalForAllAbi = [{
'inputs': [{ 'internalType': 'address', 'name': 'operator', 'type': 'address' }, {
'internalType': 'bool',
'name': 'approved',
'type': 'bool',
}],
'name': 'setApprovalForAll',
'outputs': [],
'stateMutability': 'nonpayable',
'type': 'function',
}] as unknown as EvmABI;
15 changes: 2 additions & 13 deletions src/antelope/types/IndexerTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface IndexerNftResult {
// results from the /contract/{address}/nfts endpoint
export interface IndexerCollectionNftResult extends IndexerNftResult {
supply?: number; // present only for ERC1155
owner?: string; // present only for ERC721
}

// results from the /account/{address}/nfts endpoint
Expand Down Expand Up @@ -179,7 +180,7 @@ export interface IndexerErc721AllowanceResult extends IndexerAllowanceResult {
approved: boolean; // whether the user has approved the spender
operator: string; // address of the spender contract

tokenId?: string; // only present if single === true
tokenId?: string | number; // only present if single === true
}

export interface IndexerErc1155AllowanceResult extends IndexerAllowanceResult {
Expand Down Expand Up @@ -209,15 +210,3 @@ export interface IndexerAllowanceResponseErc1155 {
}

export type IndexerAllowanceResponse = IndexerAllowanceResponseErc20 | IndexerAllowanceResponseErc721 | IndexerAllowanceResponseErc1155;

export function isIndexerAllowanceResponseErc20(response: IndexerAllowanceResponse): response is IndexerAllowanceResponseErc20 {
return (response as IndexerAllowanceResponseErc20).results.some(result => result.spender);
}

export function isIndexerAllowanceResponseErc721(response: IndexerAllowanceResponse): response is IndexerAllowanceResponseErc721 {
return (response as IndexerAllowanceResponseErc721).results.some(result => result.hasOwnProperty('single') || result.hasOwnProperty('tokenId'));
}

export function isIndexerAllowanceResponseErc1155(response: IndexerAllowanceResponse): response is IndexerAllowanceResponseErc1155 {
return !isIndexerAllowanceResponseErc20(response) && !isIndexerAllowanceResponseErc721(response);
}
Loading
Loading