Merge pull request #11 from andrei0x309/dev/6

chore: changes for 1.4.5
This commit is contained in:
Andrei O. 2024-09-12 17:07:06 +03:00 committed by GitHub
commit 1bdc7d7b37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 805 additions and 82 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## Manifest Version 1.4.5
- improved gas estimation for transactions
- fixed paste buttons with new Ionic version
- improved error display
- added personal sign view for signing messages with the private key
- changed send token view to allow sending ERC20 besides native tokens
## Manifest Version 1.4.4 ## Manifest Version 1.4.4
- added QR scaner for easier sign in with farcaster - added QR scaner for easier sign in with farcaster

View File

@ -3,8 +3,8 @@
"name": "__MSG_appName__", "name": "__MSG_appName__",
"description": "__MSG_appDesc__", "description": "__MSG_appDesc__",
"default_locale": "en", "default_locale": "en",
"version": "1.4.4", "version": "1.4.5",
"version_name": "1.4.4", "version_name": "1.4.5",
"icons": { "icons": {
"16": "assets/extension-icon/wallet_16.png", "16": "assets/extension-icon/wallet_16.png",
"32": "assets/extension-icon/wallet_32.png", "32": "assets/extension-icon/wallet_32.png",

View File

@ -36,6 +36,10 @@ const routes: Array<RouteRecordRaw> = [
path: '/farcaster-actions', path: '/farcaster-actions',
component: () => import('@/views/FarcasterActions.vue'), component: () => import('@/views/FarcasterActions.vue'),
}, },
{
path: '/personal-sign',
component: () => import('@/views/PersonalSign.vue'),
},
{ {
path: '/tabs/', path: '/tabs/',
component: AppTabs, component: AppTabs,

View File

@ -19,3 +19,62 @@ export const FARCASTER_PARTIAL_KEY_ABI = [
"type": "function" "type": "function"
} }
] ]
export const ERC20_PARTIAL_ABI = [
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
}
]

View File

@ -9,3 +9,5 @@ export const exportFile = (fileName: string, content: string, type = 'json') =>
link.click() link.click()
document.body.removeChild(link) document.body.removeChild(link)
} }
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

View File

@ -281,7 +281,15 @@ export const copyText = async (address: string, toastRef: Ref<boolean>) => {
export const getUrl = (url: string) => chrome.runtime.getURL(url) export const getUrl = (url: string) => chrome.runtime.getURL(url)
export const paste = (id: string) => { export const paste = (id: string) => {
const el = document.getElementById(id) const el = document.querySelector(`#${id} input.native-input`) as HTMLInputElement
if(el){
el.focus();
(document as any)?.execCommand('paste')
}
}
export const pasteTextArea = (id: string) => {
const el = document.querySelector(`#${id} textarea`) as HTMLTextAreaElement
if(el){ if(el){
el.focus(); el.focus();
(document as any)?.execCommand('paste') (document as any)?.execCommand('paste')

View File

@ -4,6 +4,8 @@ import { mainNets } from '@/utils/networks';
let provider: ethers.JsonRpcProvider | null = null let provider: ethers.JsonRpcProvider | null = null
const bigIntMax = (...args: bigint[]) => args.reduce((m, e) => e > m ? e : m);
export const getCurrentProvider = async () => { export const getCurrentProvider = async () => {
const network = await getSelectedNetwork() const network = await getSelectedNetwork()
if (provider) { if (provider) {
@ -77,7 +79,9 @@ export const getBalance = async () =>{
export const getGasPrice = async () => { export const getGasPrice = async () => {
const { provider } = await getCurrentProvider() const { provider } = await getCurrentProvider()
const feed = await provider.getFeeData() const feed = await provider.getFeeData()
const gasPrice = feed.maxFeePerGas ?? feed.gasPrice ?? 0n const gasPrices = [ feed.gasPrice, feed.maxFeePerGas, feed.maxPriorityFeePerGas ].filter(Boolean).map((p: any) => BigInt(p))
const gasPriceFeed = bigIntMax(...gasPrices)
const gasPrice = gasPriceFeed + (gasPriceFeed / BigInt(25))
return { return {
price: Number(gasPrice) / 1e9, price: Number(gasPrice) / 1e9,
feed feed

View File

@ -17,7 +17,7 @@
</ion-item> </ion-item>
<ion-item v-if="!isEdit"> <ion-item v-if="!isEdit">
<ion-icon <ion-icon
style="margin-right: 0.5rem" style="margin-right: 0.5rem; cursor: pointer"
@click="paste('pastePk')" @click="paste('pastePk')"
:icon="clipboardOutline" :icon="clipboardOutline"
button button

View File

@ -13,8 +13,12 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-icon <ion-icon
style="margin-right: 0.5rem" style="margin-right: 0.5rem; cursor: pointer"
@click="paste('address')" @click="
() => {
paste('address');
}
"
:icon="clipboardOutline" :icon="clipboardOutline"
button button
/> />
@ -131,10 +135,10 @@ export default defineComponent({
const close = () => { const close = () => {
try { try {
modalController.dismiss(null, "cancel"); modalController.dismiss(null, "cancel");
} catch { } catch {
// ignore // ignore
} }
}; };
return { return {

View File

@ -27,7 +27,11 @@
></ion-input> ></ion-input>
</ion-item> </ion-item>
<ion-item button> <ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" /> <ion-icon
:icon="clipboardOutline"
@click="paste('pasteRpc')"
style="margin-right: 0.5rem; cursor: pointer"
/>
<ion-input <ion-input
label="RPC URL(*)" label="RPC URL(*)"
labelPlacement="stacked" labelPlacement="stacked"
@ -46,7 +50,11 @@
></ion-input> ></ion-input>
</ion-item> </ion-item>
<ion-item button> <ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteExplorer')" /> <ion-icon
:icon="clipboardOutline"
@click="paste('pasteExplorer')"
style="margin-right: 0.5rem; cursor: pointer"
/>
<ion-input <ion-input
label="Explorer" label="Explorer"
labelPlacement="stacked" labelPlacement="stacked"

View File

@ -34,8 +34,8 @@
<ion-textarea <ion-textarea
style="overflow-y: scroll" style="overflow-y: scroll"
aria-label="Error" aria-label="Error"
:rows="10" :rows="12"
:cols="20" :cols="60"
:value="error" :value="error"
readonly readonly
></ion-textarea> ></ion-textarea>

View File

@ -213,6 +213,12 @@ import {
IonModal, IonModal,
IonButtons, IonButtons,
IonTextarea, IonTextarea,
IonList,
IonListHeader,
IonRadioGroup,
IonLoading,
IonText,
IonRadio,
} from "@ionic/vue"; } from "@ionic/vue";
import { saveSelectedAccount, paste, replaceAccounts } from "@/utils/platform"; import { saveSelectedAccount, paste, replaceAccounts } from "@/utils/platform";
import router from "@/router"; import router from "@/router";
@ -247,6 +253,12 @@ export default defineComponent({
IonModal, IonModal,
IonButtons, IonButtons,
IonTextarea, IonTextarea,
IonList,
IonListHeader,
IonRadioGroup,
IonLoading,
IonText,
IonRadio,
}, },
setup: () => { setup: () => {
const name = ref(""); const name = ref("");

View File

@ -146,6 +146,15 @@
> >
</ion-item> </ion-item>
<ion-item style="margin-top: 0.3rem; margin-bottom: 0.3rem; text-align: center">
<ion-button
@click="goToPersonalSign"
expand="block"
style="margin: auto; width: 98%; font-size: 0.8rem; padding: 0.6rem"
>Personal Sign Messages</ion-button
>
</ion-item>
<ion-loading <ion-loading
:is-open="loading" :is-open="loading"
cssClass="my-custom-class" cssClass="my-custom-class"
@ -380,6 +389,10 @@ export default defineComponent({
router.push("/farcaster-actions"); router.push("/farcaster-actions");
}; };
const goToPersonalSign = () => {
router.push("/personal-sign");
};
const changeSelectedAccount = async (address: string) => { const changeSelectedAccount = async (address: string) => {
loading.value = true; loading.value = true;
const findIndex = accounts.value.findIndex((a) => a.address == address); const findIndex = accounts.value.findIndex((a) => a.address == address);
@ -434,6 +447,7 @@ export default defineComponent({
settings, settings,
version, version,
goToFarcasterActions, goToFarcasterActions,
goToPersonalSign,
}; };
}, },
}); });

331
src/views/PersonalSign.vue Normal file
View File

@ -0,0 +1,331 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="onCancel">Back</ion-button>
</ion-buttons>
<ion-title>Personal Sign</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label style="opacity: 0.9; font-size: 0.85rem"
>Get personal signature for custom messages, useful for some websites and
developers
</ion-label>
</ion-item>
<div
style="border: 1px solid var(--ion-color-medium-contrast);
margin: 0.6rem;
}"
>
<ion-item>
<ion-label>Selected Account: {{ selectedAccount?.name }}</ion-label>
<ion-button
@click="
() => {
accountsModal = true;
toastState = false;
}
"
>Select</ion-button
>
</ion-item>
<ion-item button>
<p style="font-size: 0.7rem; color: coral">{{ selectedAccount?.address }}</p>
</ion-item>
</div>
<ion-item
button
@click="
() => {
pasteTextArea('textToSig');
}
"
>
<ion-label>
<p>
Text for personal sign
<ion-icon
style="
display: inline-block;
cursor: pointer;
margin-right: 0.5rem;
margin-top: 0.3rem;
position: relative;
top: 0.16rem;
"
:icon="clipboardOutline"
button
/>
</p>
</ion-label>
</ion-item>
<ion-item>
<ion-textarea
style="overflow-y: scroll; width: 100%"
aria-label="Enter text to sign"
id="textToSig"
:rows="4"
:cols="8"
v-model="textToSig"
></ion-textarea>
</ion-item>
<ion-item
button
@click="
() => {
if (sig.length > 0) {
copyText(sig, getToastRef());
} else {
alertMsg = 'Please get signature first';
alertOpen = true;
}
}
"
>
<ion-label>
<p>
Signature
<ion-icon
style="
display: inline-block;
cursor: pointer;
margin-right: 0.5rem;
margin-top: 0.3rem;
position: relative;
top: 0.2rem;
"
:icon="copyOutline"
></ion-icon>
</p>
</ion-label>
</ion-item>
<ion-item>
<ion-textarea
style="overflow-y: scroll; width: 100%"
aria-label="Signature"
:rows="4"
:cols="8"
v-model="sig"
></ion-textarea>
</ion-item>
<ion-item>
<ion-button @click="onCancel" color="light">Cancel</ion-button>
<ion-button @click="getSignature">Get Signature</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
:header="alertHeader"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="
() => {
alertOpen = false;
alertHeader = 'Error';
exitWallet && (window as any)?.close();
}
"
></ion-alert>
<ion-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
:duration="1500"
></ion-toast>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonIcon,
onIonViewWillEnter,
IonModal,
IonButtons,
IonTextarea,
IonToast,
modalController,
} from "@ionic/vue";
import {
saveSelectedAccount,
pasteTextArea,
replaceAccounts,
unBlockLockout,
} from "@/utils/platform";
import router from "@/router";
import type { Account } from "@/extension/types";
import { triggerListner } from "@/extension/listners";
import { copyOutline } from "ionicons/icons";
import { clipboardOutline } from "ionicons/icons";
import { getAccounts, getSelectedAccount, copyText } from "@/utils/platform";
import { signMsg } from "@/utils/wallet";
import UnlockModal from "@/views/UnlockModal.vue";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonIcon,
IonModal,
IonButtons,
IonTextarea,
IonToast,
},
setup: () => {
const textToSig = ref("");
const sig = ref("");
const alertOpen = ref(false);
const alertMsg = ref("");
const swiwModal = ref(false);
const deepLink = ref("");
const swloading = ref(false);
const warpcastLoading = ref(false);
const exitWallet = ref(false);
const alertHeader = ref("Error");
const loading = ref(false);
const accounts = ref([]) as Ref<Account[]>;
const accountsModal = ref(false) as Ref<boolean>;
const selectedAccount = (ref(null) as unknown) as Ref<Account>;
const toastState = ref(false);
const getToastRef = () => toastState;
const loadData = () => {
loading.value = true;
const pAccounts = getAccounts();
const pSelectedAccount = getSelectedAccount();
Promise.all([pAccounts, pSelectedAccount]).then((res) => {
accounts.value = res[0];
selectedAccount.value = res[1];
loading.value = false;
});
};
onIonViewWillEnter(() => {
loadData();
});
const onCancel = () => {
router.push("/tabs/home");
};
const changeSelectedAccount = async (address: string) => {
loading.value = true;
const findIndex = accounts.value.findIndex((a) => a.address == address);
if (findIndex > -1) {
selectedAccount.value = accounts.value[findIndex];
accounts.value = accounts.value.filter((a) => a.address !== address);
accounts.value.unshift(selectedAccount.value);
const newAccounts = [...accounts.value];
await Promise.all([
saveSelectedAccount(selectedAccount.value),
replaceAccounts(newAccounts),
]);
triggerListner("accountsChanged", [newAccounts.map((a) => a.address)?.[0]]);
}
accountsModal.value = false;
loading.value = false;
};
const openModal = async () => {
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: "transaction",
},
});
modal.present();
const { role } = await modal.onWillDismiss();
if (role === "confirm") return true;
return false;
};
const getSignature = async () => {
if (!selectedAccount.value) {
alertMsg.value = "Please select an account";
alertOpen.value = true;
return;
}
if (!textToSig.value) {
alertMsg.value = "Please enter text to sign";
alertOpen.value = true;
return;
}
if ((selectedAccount.value.pk ?? "").length !== 66) {
const modalResult = await openModal();
if (modalResult) {
unBlockLockout();
loading.value = true;
} else {
onCancel();
}
} else {
unBlockLockout();
}
try {
const signature = await signMsg(textToSig.value);
sig.value = signature;
} catch (e) {
console.error(e);
alertMsg.value = "Error getting signature";
alertOpen.value = true;
}
};
return {
textToSig,
sig,
onCancel,
alertOpen,
alertMsg,
clipboardOutline,
pasteTextArea,
accountsModal,
changeSelectedAccount,
selectedAccount,
accounts,
copyOutline,
toastState,
deepLink,
swiwModal,
swloading,
warpcastLoading,
window,
exitWallet,
alertHeader,
getSignature,
getToastRef,
copyText,
};
},
});
</script>

View File

@ -20,7 +20,11 @@
<ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button> <ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-icon :icon="clipboardOutline" @click="paste('pasteContract')" /> <ion-icon
:icon="clipboardOutline"
@click="paste('pasteContract')"
style="margin-right: 0.5rem; cursor: pointer"
/>
<ion-input <ion-input
label="Contract Address(*)" label="Contract Address(*)"
label-placement="stacked" label-placement="stacked"

View File

@ -2,79 +2,199 @@
<ion-page> <ion-page>
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title>Send Native Token</ion-title> <ion-title>Send Tokens</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<ion-item> <ion-segment
<ion-label>Current Network</ion-label> style="width: auto; padding: 0.5rem; margin: 0.5rem"
</ion-item> :value="currentSegment"
<template v-if="selectedNetwork?.name"> mode="ios"
@ion-change="segmentChange"
>
<ion-segment-button value="native">
<ion-label>Native</ion-label>
</ion-segment-button>
<ion-segment-button value="erc20">
<ion-label>ERC20</ion-label>
</ion-segment-button>
</ion-segment>
<template v-if="currentSegment === 'native'">
<ion-item> <ion-item>
Name: <b>{{ selectedNetwork.name }}</b> <ion-label>Current Network</ion-label>
</ion-item> </ion-item>
<template v-if="selectedNetwork?.name">
<ion-item>
Name: <b>{{ selectedNetwork.name }}</b>
</ion-item>
<ion-item>
ID: <b>{{ selectedNetwork.chainId }}</b>
</ion-item>
</template>
<hr />
<ion-item> <ion-item>
ID: <b>{{ selectedNetwork.chainId }}</b> <ion-label>Current Address</ion-label>
</ion-item>
<ion-item v-if="selectedAccount?.address">
<b style="font-size: 0.8rem">{{ selectedAccount?.address }}</b>
</ion-item>
<hr />
<ion-item>
<ion-label>Current Balance</ion-label>
</ion-item>
<ion-item v-if="currentBalance">
<b>{{ currentBalance.toFixed(8) }}</b>
</ion-item>
<hr />
<ion-item>
<ion-label>Send To Address:</ion-label>
</ion-item>
<ion-item>
<ion-input
aria-label="address"
style="font-size: 0.8rem"
id="pasteAddress"
v-model="sendTo"
></ion-input>
<ion-icon
style="margin-right: 0.5rem; cursor: pointer"
@click="paste('pasteAddress')"
:icon="clipboardOutline"
button
/>
</ion-item>
<ion-item button>
<ion-button @click="openModalAddContact()">
Load address from contacts
</ion-button>
</ion-item>
<ion-item>
<ion-label>Amount (e.g. 1.2):</ion-label>
</ion-item>
<ion-item>
<ion-input
aria-label="Amount (e.g. 1.2)"
type="number"
id="amount"
v-model="amount"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="promptTransaction">Prompt Transaction</ion-button>
</ion-item> </ion-item>
</template> </template>
<hr /> <template v-else>
<ion-item> <ion-item>
<ion-label>Current Address</ion-label> <ion-label>ERC20 Token</ion-label>
</ion-item> </ion-item>
<ion-item v-if="selectedAccount?.address"> <ion-item>
<b style="font-size: 0.8rem">{{ selectedAccount?.address }}</b> <ion-input
</ion-item> aria-label="ERC20 Token"
<hr /> type="text"
<ion-item> id="erc20"
<ion-label>Current Balance</ion-label> v-model="erc20"
</ion-item> ></ion-input>
<ion-item v-if="currentBalance"> <ion-icon
<b>{{ currentBalance.toFixed(8) }}</b> style="margin-right: 0.5rem; cursor: pointer"
</ion-item> @click="paste('erc20')"
<hr /> :icon="clipboardOutline"
button
/>
</ion-item>
<ion-item> <ion-item button>
<ion-label>Send To Address:</ion-label> <ion-button @click="openModalAddContact(true)">
</ion-item> Load address from contacts
</ion-button>
</ion-item>
<ion-item> <ion-item>
<ion-input <ion-label>Send To Address:</ion-label>
aria-label="address" </ion-item>
style="font-size: 0.8rem"
id="pasteAddress"
v-model="sendTo"
></ion-input>
<ion-icon
style="margin-right: 0.5rem"
@click="paste('pasteAddress')"
:icon="clipboardOutline"
button
/>
</ion-item>
<ion-item button> <ion-item>
<ion-button @click="openModalAddContact()"> <ion-input
Load address from contacts aria-label="address"
</ion-button> style="font-size: 0.8rem"
</ion-item> id="pasteAddress"
v-model="sendTo"
></ion-input>
<ion-icon
style="margin-right: 0.5rem; cursor: pointer"
@click="paste('pasteAddress')"
:icon="clipboardOutline"
button
/>
</ion-item>
<ion-item> <ion-item button>
<ion-label>Amount (e.g. 1.2):</ion-label> <ion-button @click="openModalAddContact()">
</ion-item> Load address from contacts
</ion-button>
</ion-item>
<ion-item> <ion-item>
<ion-input <ion-button
aria-label="Amount (e.g. 1.2)" @click="
type="number" async () => {
id="amount" if (!erc20) {
v-model="amount" alertOpen = true;
></ion-input> alertMsg = 'Invalid ERC20 address';
</ion-item> return;
}
if (loading) return;
loading = true;
await wait(100);
await balanceOfERC20();
}
"
>
<svg
height="24px"
width="24px"
id="Layer_1"
style="enable-background: new 0 0 512 512"
version="1.1"
viewBox="0 0 512 512"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path
d="M384,352l96-109.3h-66.1C407.1,141.8,325,64,223.2,64C117.8,64,32,150.1,32,256s85.8,192,191.2,192 c43.1,0,83.8-14.1,117.7-40.7l7.5-5.9l-43.2-46.2l-6.2,4.6c-22.1,16.3-48.3,24.9-75.8,24.9C152.6,384.7,95.1,327,95.1,256 c0-71,57.5-128.7,128.1-128.7c66.4,0,120.7,50,127.4,115.3h-74.1L384,352z"
/>
</svg>
</ion-button>
<ion-label>Current Balance</ion-label>
<b v-if="currentBalanceERC20">{{ currentBalanceERC20.toFixed(8) }}</b>
<b v-else-if="currentBalanceERC20 === null">Not Fetched</b>
<b v-else-if="currentBalanceERC20 === 0">0</b>
</ion-item>
<ion-item> <ion-item>
<ion-button @click="promptTransaction">Prompt Transaction</ion-button> <ion-label>Amount (e.g. 1.2):</ion-label>
</ion-item> </ion-item>
<ion-item>
<ion-input
aria-label="Amount (e.g. 1.2)"
type="number"
v-model="erc20Amount"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="promptTransactionERC20">Prompt Transaction</ion-button>
</ion-item>
</template>
<ion-alert <ion-alert
:is-open="alertOpen" :is-open="alertOpen"
@ -114,6 +234,8 @@ import {
onIonViewWillEnter, onIonViewWillEnter,
IonLoading, IonLoading,
modalController, modalController,
IonSegmentButton,
IonSegment,
// IonModal, // IonModal,
// IonButtons, // IonButtons,
// IonTextarea, // IonTextarea,
@ -137,9 +259,18 @@ import {
import { clipboardOutline } from "ionicons/icons"; import { clipboardOutline } from "ionicons/icons";
import type { Network, Account } from "@/extension/types"; import type { Network, Account } from "@/extension/types";
import { walletPromptSendTx } from "@/extension/userRequest"; import { walletPromptSendTx } from "@/extension/userRequest";
import { isAddress, formatEther, parseEther } from "ethers"; import {
import { getTxCount, getBalance } from "@/utils/wallet"; isAddress,
formatEther,
parseEther,
Contract,
formatUnits,
parseUnits,
} from "ethers";
import { getTxCount, getBalance, getCurrentProvider } from "@/utils/wallet";
import SelectedContacts from "./ContactsSelect.vue"; import SelectedContacts from "./ContactsSelect.vue";
import { ERC20_PARTIAL_ABI } from "@/utils/abis";
import { wait } from "@/utils/misc";
// import { getFromMnemonic } from "@/utils/wallet"; // import { getFromMnemonic } from "@/utils/wallet";
@ -157,6 +288,8 @@ export default defineComponent({
IonAlert, IonAlert,
IonIcon, IonIcon,
IonLoading, IonLoading,
IonSegmentButton,
IonSegment,
// IonModal, // IonModal,
// IonButtons, // IonButtons,
// IonTextarea, // IonTextarea,
@ -171,10 +304,14 @@ export default defineComponent({
const alertTitle = ref("Error"); const alertTitle = ref("Error");
const loading = ref(true); const loading = ref(true);
const amount = ref(0); const amount = ref(0);
const erc20Amount = ref(0);
const selectedNetwork = (ref(null) as unknown) as Ref<Network>; const selectedNetwork = (ref(null) as unknown) as Ref<Network>;
const selectedAccount = (ref(null) as unknown) as Ref<Account>; const selectedAccount = (ref(null) as unknown) as Ref<Account>;
const currentBalance = ref(0); const currentBalance = ref(0);
const currentBalanceERC20 = ref(null) as Ref<number | null>;
const loadingSend = ref(false); const loadingSend = ref(false);
const currentSegment = ref("native");
const erc20 = ref("");
// let accountsProm: Promise<Account[] | undefined>; // let accountsProm: Promise<Account[] | undefined>;
// let settingsProm: Promise<Settings | undefined>; // let settingsProm: Promise<Settings | undefined>;
@ -197,6 +334,10 @@ export default defineComponent({
// return false; // return false;
// }; // };
const segmentChange = (e: CustomEvent) => {
currentSegment.value = e.detail.value;
};
onIonViewWillEnter(async () => { onIonViewWillEnter(async () => {
try { try {
selectedNetwork.value = await getSelectedNetwork(); selectedNetwork.value = await getSelectedNetwork();
@ -210,6 +351,24 @@ export default defineComponent({
loading.value = false; loading.value = false;
}); });
const balanceOfERC20 = async () => {
try {
loading.value = true;
const provider = (await getCurrentProvider()).provider;
const erc20Contract = new Contract(erc20.value, ERC20_PARTIAL_ABI, provider);
const decimals = await erc20Contract.decimals();
const balance = await erc20Contract.balanceOf(selectedAccount.value.address);
currentBalanceERC20.value = Number(formatUnits(balance, decimals));
return currentBalanceERC20.value;
} catch (e) {
// ignore
currentBalanceERC20.value = null;
return null;
} finally {
loading.value = false;
}
};
const promptTransaction = async () => { const promptTransaction = async () => {
alertTitle.value = "Error"; alertTitle.value = "Error";
if ( if (
@ -272,7 +431,97 @@ export default defineComponent({
loading.value = false; loading.value = false;
}; };
const openModalAddContact = async () => { const promptTransactionERC20 = async () => {
alertTitle.value = "Error";
if (
sendTo.value?.toLocaleLowerCase() ===
selectedAccount.value.address?.toLocaleLowerCase()
) {
alertOpen.value = true;
alertMsg.value = "Cannot send to self";
return;
}
if (!isAddress(sendTo.value)) {
alertOpen.value = true;
alertMsg.value = "Invalid send address";
return;
}
if (!isAddress(erc20.value)) {
alertOpen.value = true;
alertMsg.value = "Invalid ERC20 address";
return;
}
if (erc20Amount.value <= 0) {
alertOpen.value = true;
alertMsg.value = "Amount must be greater than 0";
return;
}
// get current erc 20 balance
try {
const balance = await balanceOfERC20();
if (balance === null) {
throw new Error("Invalid ERC20 address or balance");
}
} catch (e) {
alertOpen.value = true;
alertMsg.value = "Invalid ERC20 address or balance";
}
if (Number(amount.value) >= Number(currentBalanceERC20.value)) {
alertOpen.value = true;
alertMsg.value = "Insufficient balance";
return;
}
loading.value = true;
let tx;
try {
const provider = (await getCurrentProvider()).provider;
const erc20Contract = new Contract(erc20.value, ERC20_PARTIAL_ABI, provider);
const decimals = await erc20Contract.decimals();
const value = parseUnits(erc20Amount.value.toString(), decimals).toString();
tx = {
from: selectedAccount.value.address,
to: erc20.value,
gasLimit: "0x0",
gasPrice: "0x0",
data: erc20Contract.interface.encodeFunctionData("transfer", [
sendTo.value,
value,
]),
};
} catch (e) {
alertOpen.value = true;
alertMsg.value = "Error populating transaction";
loading.value = false;
return;
}
const result = (await walletPromptSendTx(tx)) as {
error?: string;
};
if (result?.error) {
alertOpen.value = true;
alertMsg.value = "Error sending transaction to chain";
loading.value = false;
return;
} else {
alertTitle.value = "OK";
alertOpen.value = true;
alertMsg.value = "Transaction sent successfully";
currentBalanceERC20.value =
Number(currentBalanceERC20.value) - Number(erc20Amount.value);
}
loadingSend.value = false;
loading.value = false;
};
const openModalAddContact = async (isErc20 = false) => {
const modal = await modalController.create({ const modal = await modalController.create({
component: SelectedContacts, component: SelectedContacts,
componentProps: {}, componentProps: {},
@ -282,7 +531,11 @@ export default defineComponent({
const { data, role } = await modal.onWillDismiss(); const { data, role } = await modal.onWillDismiss();
if (role === "confirm") { if (role === "confirm") {
sendTo.value = data.address; if (isErc20) {
erc20.value = data.address;
} else {
sendTo.value = data.address;
}
} }
}; };
@ -302,6 +555,14 @@ export default defineComponent({
selectedAccount, selectedAccount,
selectedNetwork, selectedNetwork,
openModalAddContact, openModalAddContact,
segmentChange,
currentSegment,
erc20,
promptTransactionERC20,
balanceOfERC20,
currentBalanceERC20,
wait,
erc20Amount,
}; };
}, },
}); });

View File

@ -15,8 +15,8 @@
label="Error:" label="Error:"
labelPlacement="stacked" labelPlacement="stacked"
style="overflow-y: scroll" style="overflow-y: scroll"
:rows="10" :rows="18"
:cols="20" :cols="70"
:value="error" :value="error"
readonly readonly
></ion-textarea> ></ion-textarea>

View File

@ -20,7 +20,11 @@
<ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button> <ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-icon :icon="clipboardOutline" @click="paste('pasteContract')" /> <ion-icon
:icon="clipboardOutline"
@click="paste('pasteContract')"
style="margin-right: 0.5rem; cursor: pointer"
/>
<ion-input <ion-input
label="Contract Address(*)" label="Contract Address(*)"
label-placement="stacked" label-placement="stacked"