diff --git a/src/composables/maxAmount.ts b/src/composables/maxAmount.ts index 7ded9fa2f..85c3ce3e6 100644 --- a/src/composables/maxAmount.ts +++ b/src/composables/maxAmount.ts @@ -7,6 +7,7 @@ import { Ref, } from 'vue'; import BigNumber from 'bignumber.js'; +import { debounce } from 'lodash-es'; import { Contract, ContractMethodsBase, @@ -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; + 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(new BigNumber(0)); + const total = ref(new BigNumber(0)); + const gasUsed = ref(0); + const gasPrice = ref(0); const selectedTokenBalance = ref(new BigNumber(0)); let tokenInstance: Contract | null; const selectedAssetDecimals = ref(0); @@ -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; @@ -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; } @@ -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, @@ -152,7 +198,10 @@ export function useMaxAmount({ formModel }: MaxAmountOptions) { onBeforeUnmount(() => clearInterval(updateTokenBalanceInterval)); return { - max, fee, + gasPrice, + gasUsed, + max, + total, }; } diff --git a/src/composables/multisigTransactions.ts b/src/composables/multisigTransactions.ts index 4b1e1049a..97383c995 100644 --- a/src/composables/multisigTransactions.ts +++ b/src/composables/multisigTransactions.ts @@ -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'; @@ -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(); @@ -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; @@ -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, + }; } /** diff --git a/src/constants/stubs.ts b/src/constants/stubs.ts index c900387d0..501e32e01 100644 --- a/src/constants/stubs.ts +++ b/src/constants/stubs.ts @@ -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', diff --git a/src/popup/components/Modals/ConfirmTransactionSign.vue b/src/popup/components/Modals/ConfirmTransactionSign.vue index e6369b28f..274e4a100 100644 --- a/src/popup/components/Modals/ConfirmTransactionSign.vue +++ b/src/popup/components/Modals/ConfirmTransactionSign.vue @@ -90,6 +90,39 @@ /> + + + + + + + + (); const app = computed(() => popupProps.value?.app); @@ -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}`)) { @@ -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, @@ -549,6 +590,8 @@ export default defineComponent({ }); return { + AE_SYMBOL, + aettosToAe, AnimatedSpinner, PAYLOAD_FIELD, PROTOCOLS, @@ -561,6 +604,8 @@ export default defineComponent({ error, executionCost, filteredTxFields, + gasCost, + gasPrice, getLabels, getTxKeyLabel, getTxAssetSymbol, diff --git a/src/popup/components/TransactionDetailsBase.vue b/src/popup/components/TransactionDetailsBase.vue index 7f6108c9c..4d8b719b7 100644 --- a/src/popup/components/TransactionDetailsBase.vue +++ b/src/popup/components/TransactionDetailsBase.vue @@ -110,6 +110,12 @@ /> + +