Skip to content

Commit

Permalink
Merge pull request #3073 from superhero-com/feature/improve-total-cal…
Browse files Browse the repository at this point in the history
…culation-transfer-modal

Improve total calculation transfer modal
  • Loading branch information
CedrikNikita authored Jun 25, 2024
2 parents 17f673a + 6aaeee4 commit 78b44c5
Show file tree
Hide file tree
Showing 13 changed files with 287 additions and 64 deletions.
111 changes: 80 additions & 31 deletions src/composables/maxAmount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Ref,
} from 'vue';
import BigNumber from 'bignumber.js';
import { debounce } from 'lodash-es';
import {
Contract,
ContractMethodsBase,
Expand All @@ -18,32 +19,40 @@ import {
unpackTx,
} from '@aeternity/aepp-sdk';

import type { IFormModel } from '@/types';
import type { IFormModel, IMultisigAccount } from '@/types';
import { executeAndSetInterval, isUrlValid } from '@/utils';
import { PROTOCOLS } from '@/constants';
import { STUB_CALL_DATA, STUB_CONTRACT_ADDRESS } from '@/constants/stubs';
import { STUB_TIP_NOTE } from '@/constants/stubs';
import { AE_COIN_PRECISION, AE_CONTRACT_ID } from '@/protocols/aeternity/config';
import FungibleTokenFullInterfaceACI from '@/protocols/aeternity/aci/FungibleTokenFullInterfaceACI.json';
import { aettosToAe } from '@/protocols/aeternity/helpers';

import { useAeSdk } from './aeSdk';
import { useBalances } from './balances';
import { useAccounts } from './accounts';
import { useMultisigTransactions } from './multisigTransactions';
import { useTippingContracts } from './tippingContracts';

export interface MaxAmountOptions {
formModel: Ref<IFormModel>;
multisigVault?: IMultisigAccount;
}

/**
* Composable that allows to use real max amount of selected token
* considering the fee that needs to be paid.
*/
export function useMaxAmount({ formModel }: MaxAmountOptions) {
export function useMaxAmount({ formModel, multisigVault }: MaxAmountOptions) {
const { getAeSdk } = useAeSdk();
const { balance } = useBalances();
const { getLastActiveProtocolAccount } = useAccounts();
const { getTippingContracts } = useTippingContracts();

let updateTokenBalanceInterval: NodeJS.Timer;
const fee = ref<BigNumber>(new BigNumber(0));
const total = ref<BigNumber>(new BigNumber(0));
const gasUsed = ref(0);
const gasPrice = ref(0);
const selectedTokenBalance = ref(new BigNumber(0));
let tokenInstance: Contract<ContractMethodsBase> | null;
const selectedAssetDecimals = ref(0);
Expand All @@ -60,8 +69,7 @@ export function useMaxAmount({ formModel }: MaxAmountOptions) {
return getLastActiveProtocolAccount(PROTOCOLS.aeternity)!;
}

watch(
() => formModel.value,
const handleFormValueChangeDebounced = debounce(
async (val) => {
if (!val?.selectedAsset) {
return;
Expand All @@ -86,33 +94,63 @@ export function useMaxAmount({ formModel }: MaxAmountOptions) {
const numericAmount = (val.amount && +val.amount > 0) ? val.amount : 0;
const amount = new BigNumber(numericAmount).shiftedBy(AE_COIN_PRECISION);

if (
!isAssetAe
|| (
val.address && !isNameValid(val.address) && isUrlValid(val.address)
)
) {
let callData: Encoded.ContractBytearray = STUB_CALL_DATA;
if (tokenInstance) {
callData = tokenInstance._calldata.encode(
tokenInstance._name,
'transfer',
[account.address, amount.toFixed()],
let callResult: any;

if (!isAssetAe && tokenInstance) {
try {
callResult = await tokenInstance.transfer(
val.address ?? account.address,
amount.toFixed(),
{ callStatic: true },
);
} catch (e) {
return;
}
fee.value = BigNumber(unpackTx(
await aeSdk.buildTx({
tag: Tag.ContractCallTx,
callerId: account.address as Encoded.AccountAddress,
contractId: (isAssetAe)
? STUB_CONTRACT_ADDRESS
: val.selectedAsset.contractId as Encoded.ContractAddress,
amount: 0,
callData,
ttl: await aeSdk.getHeight({ cached: true }) + 3,
}),
Tag.ContractCallTx, // https://github.com/aeternity/aepp-sdk-js/issues/1852
).fee).shiftedBy(-AE_COIN_PRECISION);
}

if (multisigVault) {
const { proposeTx } = useMultisigTransactions();
const builtTransactionHash = await aeSdk.buildTx({
tag: Tag.SpendTx,
senderId: multisigVault.gaAccountId as Encoded.AccountAddress,
recipientId: account.address as Encoded.AccountAddress,
amount,
payload: encode(new TextEncoder().encode(val.payload), Encoding.Bytearray),
});
callResult = (await proposeTx(
builtTransactionHash,
multisigVault.contractId,
{ callStatic: true },
))?.callResult;
}

if (val.address && !isNameValid(val.address) && isUrlValid(val.address)) {
const { tippingV1, tippingV2 } = await getTippingContracts();
const tippingContract = tippingV2 || tippingV1;
try {
callResult = await tippingContract.tip(
val.address ?? account.address,
STUB_TIP_NOTE,
{
amount,
waitMined: false,
callStatic: true,
},
);
} catch (e) {
return;
}
}

if (callResult?.result) {
const aettosFee = (callResult.tx as any).fee;
gasUsed.value = +callResult.result.gasUsed.toString() ?? 0;
gasPrice.value = +aettosToAe(callResult.result.gasPrice.toString());
total.value = new BigNumber(callResult.result.gasUsed ?? 0)
.multipliedBy(callResult.result.gasPrice.toString() ?? 0)
.plus(aettosFee)
.shiftedBy(-AE_COIN_PRECISION);
fee.value = new BigNumber(aettosFee).shiftedBy(-AE_COIN_PRECISION);
return;
}

Expand All @@ -132,7 +170,15 @@ export function useMaxAmount({ formModel }: MaxAmountOptions) {
Tag.SpendTx, // https://github.com/aeternity/aepp-sdk-js/issues/1852
).fee).shiftedBy(-AE_COIN_PRECISION);
if (!minFee.isEqualTo(fee.value)) fee.value = minFee;
total.value = new BigNumber(amount).shiftedBy(-AE_COIN_PRECISION).plus(fee.value);
},
500,
{ leading: true },
);

watch(
() => formModel.value,
(val) => handleFormValueChangeDebounced(val),
{
deep: true,
immediate: true,
Expand All @@ -152,7 +198,10 @@ export function useMaxAmount({ formModel }: MaxAmountOptions) {
onBeforeUnmount(() => clearInterval(updateTokenBalanceInterval));

return {
max,
fee,
gasPrice,
gasUsed,
max,
total,
};
}
23 changes: 16 additions & 7 deletions src/composables/multisigTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
handleUnknownError,
postJson,
} from '@/utils';
import { MULTISIG_SIMPLE_GA_BYTECODE } from '@/protocols/aeternity/config';
import { MULTISIG_SIMPLE_GA_BYTECODE, AE_GET_META_TX_FEE } from '@/protocols/aeternity/config';
import { useAeNetworkSettings } from '@/protocols/aeternity/composables';
import { useAeSdk } from './aeSdk';
import { useMultisigAccounts } from './multisigAccounts';
Expand All @@ -34,7 +34,7 @@ interface InternalOptions {
const MULTISIG_TRANSACTION_EXPIRATION_HEIGHT = 480;

// TODO: calculate gas price based on node demand
const GA_META_PARAMS = { fee: 1e14, gasPrice: 1e9 };
const GA_META_PARAMS = { fee: AE_GET_META_TX_FEE, gasPrice: 1e9 };

export function useMultisigTransactions() {
const { aeActiveNetworkPredefinedSettings } = useAeNetworkSettings();
Expand Down Expand Up @@ -104,7 +104,11 @@ export function useMultisigTransactions() {
return postJson(`${aeActiveNetworkPredefinedSettings.value.multisigBackendUrl}/tx`, { body: { hash: txHash, tx } });
}

async function proposeTx(spendTx: Encoded.Transaction, contractId: Encoded.ContractAddress) {
async function proposeTx(
spendTx: Encoded.Transaction,
contractId: Encoded.ContractAddress,
options?: any,
) {
const [aeSdk, topBlockHeight] = await Promise.all([getAeSdk(), fetchCurrentTopBlockHeight()]);
const expirationHeight = topBlockHeight + MULTISIG_TRANSACTION_EXPIRATION_HEIGHT;

Expand All @@ -115,11 +119,16 @@ export function useMultisigTransactions() {
address: contractId,
});

await gaContractRpc.propose(spendTxHash, {
FixedTTL: [expirationHeight],
});
const callResult = await gaContractRpc.propose(
spendTxHash,
{ FixedTTL: [expirationHeight] },
options || {},
);

return Buffer.from(spendTxHash).toString('hex');
return {
proposeTxHash: Buffer.from(spendTxHash).toString('hex'),
callResult,
};
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/constants/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const STUB_NONCE = 10000;
export const STUB_TOKEN_CONTRACT_ADDRESS = 'ct_T6MWNrowGVC9dyTDksCBrCCSaeK3hzBMMY5hhMKwvwr8wJvM8';
export const STUB_TIPPING_CONTRACT_ID_V1 = 'ct_2Cvbf3NYZ5DLoaNYAU71t67DdXLHeSXhodkSNifhgd7Xsw28Xd';
export const STUB_TIPPING_CONTRACT_ID_V2 = 'ct_2ZEoCKcqXkbz2uahRrsWeaPooZs9SdCv6pmC4kc55rD4MhqYSu';
export const STUB_TIP_NOTE = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel interdum ligula, non consequat libero. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam congue, nibh non malesuada ornare, ante metus tempor dui, a ultrices ante ut.';

export const STUB_ACCOUNT = {
mnemonic: 'media view gym mystery all fault truck target envelope kit drop fade',
Expand Down
45 changes: 45 additions & 0 deletions src/popup/components/Modals/ConfirmTransactionSign.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,39 @@
/>
</DetailsItem>
<DetailsItem
v-if="popupProps?.tx?.gasLimit"
:value="popupProps.tx.gasLimit"
:label="$t('transaction.gasLimit')"
data-cy="gas"
/>
<DetailsItem
v-if="gasPrice"
:label="$t('pages.transactionDetails.gasPrice')"
data-cy="gas-price"
>
<template #value>
<TokenAmount
:amount="+aettosToAe(gasPrice)"
:symbol="AE_SYMBOL"
:protocol="PROTOCOLS.aeternity"
/>
</template>
</DetailsItem>
<DetailsItem
v-if="gasCost"
:label="$t('transaction.gasCost')"
data-cy="gas-cost"
>
<template #value>
<TokenAmount
:amount="+aettosToAe(gasCost)"
:symbol="AE_SYMBOL"
:protocol="PROTOCOLS.aeternity"
/>
</template>
</DetailsItem>
<DetailsItem :label="$t('transaction.fee')">
<TokenAmount
:amount="fee"
Expand Down Expand Up @@ -225,7 +258,9 @@ import {
usePopupProps,
useTransactionData,
} from '@/composables';
import { AE_SYMBOL } from '@/protocols/aeternity/config';
import {
aettosToAe,
getAeFee,
getTransactionTokenInfoResolver,
} from '@/protocols/aeternity/helpers';
Expand Down Expand Up @@ -308,6 +343,7 @@ export default defineComponent({
const loading = ref(false);
const error = ref('');
const verifying = ref(false);
const gasPrice = ref(0);
const decodedCallData = ref<AeDecodedCallData | undefined>();
const app = computed(() => popupProps.value?.app);
Expand All @@ -317,6 +353,8 @@ export default defineComponent({
: popupProps.value?.tx?.fee!);
const nameAeFee = computed(() => getAeFee(popupProps.value?.tx?.nameFee!));
const gasCost = computed(() => new BigNumber(popupProps.value?.tx?.gasLimit ?? 0)
.multipliedBy(gasPrice.value));
const appName = computed((): string | undefined => {
if (SUPERHERO_CHAT_URLS.includes(`${app.value?.protocol}//${app.value?.name}`)) {
Expand Down Expand Up @@ -465,6 +503,9 @@ export default defineComponent({
const accountAddress = getTransactionSignerAddress(popupProps.value.txBase64);
txParams.nonce = (await sdk.api.getAccountByPubkey(accountAddress)).nonce + 1;
const dryRunResult = await sdk.txDryRun(buildTx(txParams), accountAddress);
gasPrice.value = dryRunResult.callObj?.gasPrice
? +dryRunResult.callObj.gasPrice.toString()
: 0;
if (dryRunResult.callObj && dryRunResult.callObj.returnType !== 'ok') {
error.value = new ContractByteArrayEncoder().decode(
dryRunResult.callObj.returnValue as Encoded.ContractBytearray,
Expand Down Expand Up @@ -549,6 +590,8 @@ export default defineComponent({
});
return {
AE_SYMBOL,
aettosToAe,
AnimatedSpinner,
PAYLOAD_FIELD,
PROTOCOLS,
Expand All @@ -561,6 +604,8 @@ export default defineComponent({
error,
executionCost,
filteredTxFields,
gasCost,
gasPrice,
getLabels,
getTxKeyLabel,
getTxAssetSymbol,
Expand Down
6 changes: 6 additions & 0 deletions src/popup/components/TransactionDetailsBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@
/>
</div>
<DetailsItem
v-if="transaction.tx.function"
:label="$t('modals.confirmTransactionSign.functionName')"
:value="transaction.tx.function"
/>
<DetailsItem
v-if="amount"
:label="$t('common.amount')"
Expand Down
3 changes: 2 additions & 1 deletion src/popup/components/TransferSend/TransferReviewBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@

<DetailsItem
class="details-item"
:label="$t('transaction.fee')"
:label="feeLabel"
>
<template #value>
<TokenAmount
Expand Down Expand Up @@ -124,6 +124,7 @@ export default defineComponent({
subtitle: { type: String, default: tg('pages.send.checkalert') },
senderLabel: { type: String, default: tg('pages.send.sender') },
amountLabel: { type: String, default: tg('common.amount') },
feeLabel: { type: String, default: tg('transaction.fee') },
baseTokenSymbol: { type: String, required: true },
transferData: { type: Object as PropType<TransferFormModel>, required: true },
protocol: { type: String as PropType<Protocol>, required: true },
Expand Down
7 changes: 7 additions & 0 deletions src/popup/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,9 +472,16 @@
"transaction": {
"fee": "Transaction fee",
"estimatedFee": "Estimated transaction fee",
"estimatedGasUsed": "Estimated gas used",
"maxFee": "Maximum transaction fee",
"advancedDetails": "Advanced transaction details",
"data": "Transaction data",
"gasCost": "Gas cost",
"gasLimit": "Gas limit",
"proposalEstimatedGasUsed": "Proposal estimated gas used",
"proposalGasCost": "Proposal gas cost",
"proposalTransactionFee": "Proposal transaction fee",
"proposalTotal": "Proposal total",
"type": {
"spendTx": "Spend",
"contractCreateTx": "Contract create",
Expand Down
Loading

0 comments on commit 78b44c5

Please sign in to comment.