Skip to content

Commit

Permalink
feat: move fungibleTokens to composable
Browse files Browse the repository at this point in the history
  • Loading branch information
konradodo authored and peronczyk committed Oct 25, 2023
1 parent 217334a commit a65630c
Show file tree
Hide file tree
Showing 34 changed files with 543 additions and 495 deletions.
317 changes: 317 additions & 0 deletions src/composables/fungibleTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
import { watch } from 'vue';
import camelCaseKeysDeep from 'camelcase-keys-deep';
import BigNumber from 'bignumber.js';
import { Encoded, Encoding } from '@aeternity/aepp-sdk';
import { fetchAllPages, handleUnknownError, toShiftedBigNumber } from '@/utils';
import type {
BigNumberPublic,
IDefaultComposableOptions,
IToken,
ITokenBalanceResponse,
ITokenList,
ITransaction,
TokenPair,
} from '@/types';
import { PROTOCOL_AETERNITY, STORAGE_KEYS, TX_DIRECTION } from '@/constants';
import FungibleTokenFullInterfaceACI from '@/lib/contracts/FungibleTokenFullInterfaceACI.json';
import AedexV2PairACI from '@/lib/contracts/AedexV2PairACI.json';
import ZeitTokenACI from '@/lib/contracts/FungibleTokenFullACI.json';

import { aettosToAe, calculateSupplyAmount, categorizeContractCallTxObject } from '@/protocols/aeternity/helpers';
import { AE_SYMBOL } from '@/protocols/aeternity/config';

import { useAccounts } from './accounts';
import { useAeSdk } from './aeSdk';
import { useMiddleware } from './middleware';
import { useTippingContracts } from './tippingContracts';
import { createNetworkWatcher } from './networks';
import { createPollingBasedOnMountedComponents } from './composablesHelpers';
import { useStorageRef } from './storageRef';

/**
* List of all custom tokens available (currently only AE network).
* As this list is quite big (hundreds of items) it requires processing optimizations.
*/
const availableTokens = useStorageRef<ITokenList>(
{},
STORAGE_KEYS.fungibleTokenList,
);

/**
* List of tokens (assets) owned by active account with the balance value
*/
const tokenBalances = useStorageRef<Record<string, IToken[]>>(
{},
STORAGE_KEYS.fungibleTokenBalances,
);

const { onNetworkChange } = createNetworkWatcher();
const availableTokensPooling = createPollingBasedOnMountedComponents(60000);
const tokenBalancesPooling = createPollingBasedOnMountedComponents(10000);

export function useFungibleTokens({ store }: IDefaultComposableOptions) {
const { getAeSdk } = useAeSdk({ store });
const { fetchFromMiddleware } = useMiddleware();
const { tippingContractAddresses } = useTippingContracts({ store });
const {
isLoggedIn,
aeAccounts,
getLastActiveProtocolAccount,
} = useAccounts();

function getAccountTokenBalances(address?: string): IToken[] {
const account = getLastActiveProtocolAccount(PROTOCOL_AETERNITY);
return tokenBalances.value[address || account?.address!] || [];
}

async function loadAvailableTokens() {
const response: IToken[] = camelCaseKeysDeep(await fetchAllPages(
() => fetchFromMiddleware('/v2/aex9?by=name&limit=100&direction=forward'),
fetchFromMiddleware,
));

if (!response.length) {
availableTokens.value = {};
}

availableTokens.value = response.reduce((accumulator, token) => {
// eslint-disable-next-line no-param-reassign
accumulator[token.contractId] = token;
return accumulator;
}, {} as ITokenList);
}

async function loadTokenBalances() {
if (!isLoggedIn.value) {
return;
}
const addresses = aeAccounts.value.map((account) => account.address);

await Promise.all(addresses.map(async (address) => {
try {
const tokens: ITokenBalanceResponse[] = camelCaseKeysDeep(await fetchAllPages(
() => fetchFromMiddleware(`/v2/aex9/account-balances/${address}?limit=100`),
fetchFromMiddleware,
));
if (!tokens.length) {
return;
}

tokenBalances.value[address] = tokens
.filter(({ contractId }) => availableTokens.value[contractId])
.map(({ amount, contractId }): IToken => {
const availableToken = availableTokens.value[contractId];
const balance = toShiftedBigNumber(amount!, -availableToken.decimals);
const convertedBalance = Number(balance.toFixed(2));

return {
...availableToken, // TODO store the balance and amount separately from asset data
amount,
convertedBalance,
};
});
} catch (error) {
handleUnknownError(error);
}
}));
}

async function createOrChangeAllowance(contractId: string, amount: number | string) {
const aeSdk = await getAeSdk();
const account = getLastActiveProtocolAccount(PROTOCOL_AETERNITY);
const selectedToken = tokenBalances.value?.[account?.address!]
?.find((token) => token?.contractId === contractId);

const tokenContract = await aeSdk.initializeContract({
aci: FungibleTokenFullInterfaceACI,
address: selectedToken?.contractId as any,
});

const { decodedResult } = await tokenContract.allowance({
from_account: account?.address,
for_account: tippingContractAddresses?.value?.tippingV2?.replace('ct_', 'ak_'),
});

const allowanceAmount = (decodedResult !== undefined)
? new BigNumber(decodedResult)
.multipliedBy(-1)
.plus(toShiftedBigNumber(amount, selectedToken?.decimals!))
.toNumber()
: toShiftedBigNumber(amount, selectedToken?.decimals!)
.toNumber();

const getContractFunction = (tokenContract.methods as any)[
decodedResult !== undefined ? 'change_allowance' : 'create_allowance'
];

return getContractFunction(
tippingContractAddresses.value?.tippingV2?.replace(
`${Encoding.ContractAddress}_`,
`${Encoding.AccountAddress}_`,
),
allowanceAmount,
);
}

async function getContractTokenPairs(
address: Encoded.ContractAddress,
): Promise<Partial<TokenPair> & Record<string, any>> {
try {
const aeSdk = await getAeSdk();
const account = getLastActiveProtocolAccount(PROTOCOL_AETERNITY);
const tokenContract = await aeSdk.initializeContract({
aci: AedexV2PairACI,
address,
});

const [
{ decodedResult: balances },
{ decodedResult: balance },
{ decodedResult: token0 },
{ decodedResult: token1 },
{ decodedResult: reserves },
{ decodedResult: totalSupply },
] = await Promise.all([
tokenContract.balances(),
tokenContract.balance(account?.address),
tokenContract.token0(),
tokenContract.token1(),
tokenContract.get_reserves(),
tokenContract.total_supply(),
]);

return {
token0: {
...availableTokens.value?.[token0],
amount: calculateSupplyAmount(
balance,
totalSupply,
reserves.reserve0,
),
},
token1: {
...availableTokens.value?.[token1],
amount: calculateSupplyAmount(
balance,
totalSupply,
reserves.reserve1,
),
},
totalSupply,
balance,
balances,
};
} catch (error) {
return {};
}
}

async function transferToken(
tokenContractId: Encoded.ContractAddress,
toAccount: Encoded.AccountAddress,
amount: number,
option: {
waitMined: boolean,
modal: boolean,
},
) {
const aeSdk = await getAeSdk();
const tokenContract = await aeSdk.initializeContract({
aci: FungibleTokenFullInterfaceACI,
address: tokenContractId,
});
return tokenContract.transfer(toAccount, amount.toFixed(), option);
}

async function burnTriggerPoS(
address: Encoded.ContractAddress,
posAddress: string,
invoiceId: string,
amount: number,
option: {
waitMined: boolean,
modal: boolean,
},
) {
const aeSdk = await getAeSdk();
const tokenContract = await aeSdk.initializeContract({
aci: ZeitTokenACI,
address,
});
return tokenContract.burn_trigger_pos(
amount.toFixed(),
posAddress,
invoiceId,
option,
);
}

function getTxSymbol(transaction?: ITransaction) {
if (transaction?.pendingTokenTx) {
return availableTokens.value[transaction.tx.contractId]?.symbol;
}
const contractCallData = transaction?.tx && categorizeContractCallTxObject(transaction);
return availableTokens.value[contractCallData?.token!]?.symbol || AE_SYMBOL;
}

function getTxAmountTotal(
transaction: ITransaction,
direction: string = TX_DIRECTION.sent,
) {
const contractCallData = transaction.tx && categorizeContractCallTxObject(transaction);
if (contractCallData && availableTokens.value[contractCallData.token!]) {
return +toShiftedBigNumber(
contractCallData.amount || 0,
-availableTokens.value[contractCallData.token!].decimals,
);
}
const isReceived = direction === TX_DIRECTION.received;

const rawAmount = (
transaction.tx?.amount
|| (transaction.tx?.tx?.tx as any)?.amount
|| transaction.tx?.nameFee
|| 0
);

const amount: BigNumberPublic = (typeof rawAmount === 'object')
? rawAmount
: new BigNumber(Number(rawAmount));

return +aettosToAe(amount
.plus(isReceived ? 0 : transaction.tx?.fee || 0)
.plus(isReceived ? 0 : transaction.tx?.tx?.tx?.fee || 0));
}

onNetworkChange(async (network, oldNetwork) => {
const newMiddlewareUrl = network.protocols[PROTOCOL_AETERNITY].middlewareUrl;
const oldMiddlewareUrl = oldNetwork?.protocols?.[PROTOCOL_AETERNITY]?.middlewareUrl;
if (newMiddlewareUrl !== oldMiddlewareUrl) {
await loadAvailableTokens();
await loadTokenBalances();
}
});

watch(aeAccounts, (val, oldVal) => {
if (val !== oldVal) {
loadTokenBalances();
}
});

availableTokensPooling(() => loadAvailableTokens());
tokenBalancesPooling(() => loadTokenBalances());

return {
availableTokens,
tokenBalances,
burnTriggerPoS,
createOrChangeAllowance,
getAccountTokenBalances,
getContractTokenPairs,
getTxSymbol,
getTxAmountTotal,
transferToken,
loadTokenBalances,
loadAvailableTokens,
};
}
1 change: 1 addition & 0 deletions src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './transactionTokens';
export * from './transactionList';
export * from './ui';
export * from './viewport';
export * from './fungibleTokens';
export * from './coinTokensProps';
export * from './scrollTransactions';
export * from './languages';
6 changes: 3 additions & 3 deletions src/composables/latestTransactionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@/utils';
import { ProtocolAdapterFactory } from '@/lib/ProtocolAdapterFactory';
import { AE_MDW_TO_NODE_APPROX_DELAY_TIME } from '@/protocols/aeternity/config';
import { useFungibleTokens } from './fungibleTokens';
import { useAccounts } from './accounts';
import { useBalances } from './balances';
import { useTransactionTx } from './transactionTx';
Expand All @@ -40,10 +41,9 @@ export function useLatestTransactionList({ store }: IDefaultComposableOptions) {
fetchTransactions,
} = useTransactionList({ store });

const { tokenBalances } = useFungibleTokens({ store });
const btcTransactions = ref<ITransaction[]>([]);

const tokens = computed(() => store.state.fungibleTokens.tokens);

const latestTransactions = computed(() => {
const aeTransactions = Object.entries(transactions.value)
.map(([
Expand Down Expand Up @@ -131,7 +131,7 @@ export function useLatestTransactionList({ store }: IDefaultComposableOptions) {
);

watch(
tokens,
tokenBalances,
(oldTokens, newTokens) => {
if (!isEqual(oldTokens, newTokens)) {
updateTransactionListData();
Expand Down
16 changes: 12 additions & 4 deletions src/composables/tokensList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import type {
import { AE_CONTRACT_ID } from '@/protocols/aeternity/config';
import { ProtocolAdapterFactory } from '@/lib/ProtocolAdapterFactory';
import { PROTOCOL_AETERNITY } from '@/constants';
import { useCurrencies } from '@/composables/currencies';
import { useCurrencies } from './currencies';
import { useFungibleTokens } from './fungibleTokens';
import { useAccounts } from './accounts';
import { useMultisigAccounts } from './multisigAccounts';
import { useBalances } from './balances';

Expand Down Expand Up @@ -46,21 +48,27 @@ export function useTokensList({
}: UseTokensListOptions) {
const { marketData } = useCurrencies();
const { balance } = useBalances();
const { activeAccount } = useAccounts();
const { activeMultisigAccount } = useMultisigAccounts({ store });
const {
availableTokens: allAvailableTokens,
getAccountTokenBalances,
} = useFungibleTokens({ store });

const availableTokens = computed<ITokenList>(() => (
isMultisig
? []
: (store.state as any).fungibleTokens.availableTokens
? {}
: allAvailableTokens.value
));
const tokenBalances = computed<IToken[]>(() => store.getters['fungibleTokens/tokenBalances']);

const aeTokenBalance = computed((): Balance => (
isMultisig
? new BigNumber(activeMultisigAccount.value?.balance || 0)
: balance.value || new BigNumber(0)
));

const tokenBalances = computed(() => getAccountTokenBalances(activeAccount.value.address));

/**
* Returns the default aeternity meta information
*/
Expand Down
Loading

0 comments on commit a65630c

Please sign in to comment.