mirror of
https://github.com/andrei0x309/clear-wallet.git
synced 2024-11-18 23:41:10 +00:00
commit
1bdc7d7b37
@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
- added QR scaner for easier sign in with farcaster
|
||||
|
@ -3,8 +3,8 @@
|
||||
"name": "__MSG_appName__",
|
||||
"description": "__MSG_appDesc__",
|
||||
"default_locale": "en",
|
||||
"version": "1.4.4",
|
||||
"version_name": "1.4.4",
|
||||
"version": "1.4.5",
|
||||
"version_name": "1.4.5",
|
||||
"icons": {
|
||||
"16": "assets/extension-icon/wallet_16.png",
|
||||
"32": "assets/extension-icon/wallet_32.png",
|
||||
|
@ -36,6 +36,10 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: '/farcaster-actions',
|
||||
component: () => import('@/views/FarcasterActions.vue'),
|
||||
},
|
||||
{
|
||||
path: '/personal-sign',
|
||||
component: () => import('@/views/PersonalSign.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tabs/',
|
||||
component: AppTabs,
|
||||
|
@ -18,4 +18,63 @@ export const FARCASTER_PARTIAL_KEY_ABI = [
|
||||
"stateMutability": "view",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
@ -9,3 +9,5 @@ export const exportFile = (fileName: string, content: string, type = 'json') =>
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
@ -281,7 +281,15 @@ export const copyText = async (address: string, toastRef: Ref<boolean>) => {
|
||||
export const getUrl = (url: string) => chrome.runtime.getURL(url)
|
||||
|
||||
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){
|
||||
el.focus();
|
||||
(document as any)?.execCommand('paste')
|
||||
|
@ -4,6 +4,8 @@ import { mainNets } from '@/utils/networks';
|
||||
|
||||
let provider: ethers.JsonRpcProvider | null = null
|
||||
|
||||
const bigIntMax = (...args: bigint[]) => args.reduce((m, e) => e > m ? e : m);
|
||||
|
||||
export const getCurrentProvider = async () => {
|
||||
const network = await getSelectedNetwork()
|
||||
if (provider) {
|
||||
@ -77,7 +79,9 @@ export const getBalance = async () =>{
|
||||
export const getGasPrice = async () => {
|
||||
const { provider } = await getCurrentProvider()
|
||||
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 {
|
||||
price: Number(gasPrice) / 1e9,
|
||||
feed
|
||||
|
@ -17,7 +17,7 @@
|
||||
</ion-item>
|
||||
<ion-item v-if="!isEdit">
|
||||
<ion-icon
|
||||
style="margin-right: 0.5rem"
|
||||
style="margin-right: 0.5rem; cursor: pointer"
|
||||
@click="paste('pastePk')"
|
||||
:icon="clipboardOutline"
|
||||
button
|
||||
|
@ -13,8 +13,12 @@
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-icon
|
||||
style="margin-right: 0.5rem"
|
||||
@click="paste('address')"
|
||||
style="margin-right: 0.5rem; cursor: pointer"
|
||||
@click="
|
||||
() => {
|
||||
paste('address');
|
||||
}
|
||||
"
|
||||
:icon="clipboardOutline"
|
||||
button
|
||||
/>
|
||||
@ -131,10 +135,10 @@ export default defineComponent({
|
||||
|
||||
const close = () => {
|
||||
try {
|
||||
modalController.dismiss(null, "cancel");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
modalController.dismiss(null, "cancel");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -27,7 +27,11 @@
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<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
|
||||
label="RPC URL(*)"
|
||||
labelPlacement="stacked"
|
||||
@ -46,7 +50,11 @@
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<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
|
||||
label="Explorer"
|
||||
labelPlacement="stacked"
|
||||
|
@ -34,8 +34,8 @@
|
||||
<ion-textarea
|
||||
style="overflow-y: scroll"
|
||||
aria-label="Error"
|
||||
:rows="10"
|
||||
:cols="20"
|
||||
:rows="12"
|
||||
:cols="60"
|
||||
:value="error"
|
||||
readonly
|
||||
></ion-textarea>
|
||||
|
@ -213,6 +213,12 @@ import {
|
||||
IonModal,
|
||||
IonButtons,
|
||||
IonTextarea,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonRadioGroup,
|
||||
IonLoading,
|
||||
IonText,
|
||||
IonRadio,
|
||||
} from "@ionic/vue";
|
||||
import { saveSelectedAccount, paste, replaceAccounts } from "@/utils/platform";
|
||||
import router from "@/router";
|
||||
@ -247,6 +253,12 @@ export default defineComponent({
|
||||
IonModal,
|
||||
IonButtons,
|
||||
IonTextarea,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonRadioGroup,
|
||||
IonLoading,
|
||||
IonText,
|
||||
IonRadio,
|
||||
},
|
||||
setup: () => {
|
||||
const name = ref("");
|
||||
|
@ -146,6 +146,15 @@
|
||||
>
|
||||
</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
|
||||
:is-open="loading"
|
||||
cssClass="my-custom-class"
|
||||
@ -380,6 +389,10 @@ export default defineComponent({
|
||||
router.push("/farcaster-actions");
|
||||
};
|
||||
|
||||
const goToPersonalSign = () => {
|
||||
router.push("/personal-sign");
|
||||
};
|
||||
|
||||
const changeSelectedAccount = async (address: string) => {
|
||||
loading.value = true;
|
||||
const findIndex = accounts.value.findIndex((a) => a.address == address);
|
||||
@ -434,6 +447,7 @@ export default defineComponent({
|
||||
settings,
|
||||
version,
|
||||
goToFarcasterActions,
|
||||
goToPersonalSign,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
331
src/views/PersonalSign.vue
Normal file
331
src/views/PersonalSign.vue
Normal 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>
|
@ -20,7 +20,11 @@
|
||||
<ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button>
|
||||
</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
|
||||
label="Contract Address(*)"
|
||||
label-placement="stacked"
|
||||
|
@ -2,79 +2,199 @@
|
||||
<ion-page>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Send Native Token</ion-title>
|
||||
<ion-title>Send Tokens</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item>
|
||||
<ion-label>Current Network</ion-label>
|
||||
</ion-item>
|
||||
<template v-if="selectedNetwork?.name">
|
||||
<ion-segment
|
||||
style="width: auto; padding: 0.5rem; margin: 0.5rem"
|
||||
:value="currentSegment"
|
||||
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>
|
||||
Name: <b>{{ selectedNetwork.name }}</b>
|
||||
<ion-label>Current Network</ion-label>
|
||||
</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>
|
||||
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>
|
||||
</template>
|
||||
<hr />
|
||||
<ion-item>
|
||||
<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 />
|
||||
<template v-else>
|
||||
<ion-item>
|
||||
<ion-label>ERC20 Token</ion-label>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
aria-label="ERC20 Token"
|
||||
type="text"
|
||||
id="erc20"
|
||||
v-model="erc20"
|
||||
></ion-input>
|
||||
<ion-icon
|
||||
style="margin-right: 0.5rem; cursor: pointer"
|
||||
@click="paste('erc20')"
|
||||
:icon="clipboardOutline"
|
||||
button
|
||||
/>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Send To Address:</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button>
|
||||
<ion-button @click="openModalAddContact(true)">
|
||||
Load address from contacts
|
||||
</ion-button>
|
||||
</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"
|
||||
@click="paste('pasteAddress')"
|
||||
:icon="clipboardOutline"
|
||||
button
|
||||
/>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Send To Address:</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button>
|
||||
<ion-button @click="openModalAddContact()">
|
||||
Load address from contacts
|
||||
</ion-button>
|
||||
</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>
|
||||
<ion-label>Amount (e.g. 1.2):</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button>
|
||||
<ion-button @click="openModalAddContact()">
|
||||
Load address from contacts
|
||||
</ion-button>
|
||||
</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="
|
||||
async () => {
|
||||
if (!erc20) {
|
||||
alertOpen = true;
|
||||
alertMsg = 'Invalid ERC20 address';
|
||||
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-button @click="promptTransaction">Prompt Transaction</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"
|
||||
v-model="erc20Amount"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-button @click="promptTransactionERC20">Prompt Transaction</ion-button>
|
||||
</ion-item>
|
||||
</template>
|
||||
|
||||
<ion-alert
|
||||
:is-open="alertOpen"
|
||||
@ -114,6 +234,8 @@ import {
|
||||
onIonViewWillEnter,
|
||||
IonLoading,
|
||||
modalController,
|
||||
IonSegmentButton,
|
||||
IonSegment,
|
||||
// IonModal,
|
||||
// IonButtons,
|
||||
// IonTextarea,
|
||||
@ -137,9 +259,18 @@ import {
|
||||
import { clipboardOutline } from "ionicons/icons";
|
||||
import type { Network, Account } from "@/extension/types";
|
||||
import { walletPromptSendTx } from "@/extension/userRequest";
|
||||
import { isAddress, formatEther, parseEther } from "ethers";
|
||||
import { getTxCount, getBalance } from "@/utils/wallet";
|
||||
import {
|
||||
isAddress,
|
||||
formatEther,
|
||||
parseEther,
|
||||
Contract,
|
||||
formatUnits,
|
||||
parseUnits,
|
||||
} from "ethers";
|
||||
import { getTxCount, getBalance, getCurrentProvider } from "@/utils/wallet";
|
||||
import SelectedContacts from "./ContactsSelect.vue";
|
||||
import { ERC20_PARTIAL_ABI } from "@/utils/abis";
|
||||
import { wait } from "@/utils/misc";
|
||||
|
||||
// import { getFromMnemonic } from "@/utils/wallet";
|
||||
|
||||
@ -157,6 +288,8 @@ export default defineComponent({
|
||||
IonAlert,
|
||||
IonIcon,
|
||||
IonLoading,
|
||||
IonSegmentButton,
|
||||
IonSegment,
|
||||
// IonModal,
|
||||
// IonButtons,
|
||||
// IonTextarea,
|
||||
@ -171,10 +304,14 @@ export default defineComponent({
|
||||
const alertTitle = ref("Error");
|
||||
const loading = ref(true);
|
||||
const amount = ref(0);
|
||||
const erc20Amount = ref(0);
|
||||
const selectedNetwork = (ref(null) as unknown) as Ref<Network>;
|
||||
const selectedAccount = (ref(null) as unknown) as Ref<Account>;
|
||||
const currentBalance = ref(0);
|
||||
const currentBalanceERC20 = ref(null) as Ref<number | null>;
|
||||
const loadingSend = ref(false);
|
||||
const currentSegment = ref("native");
|
||||
const erc20 = ref("");
|
||||
|
||||
// let accountsProm: Promise<Account[] | undefined>;
|
||||
// let settingsProm: Promise<Settings | undefined>;
|
||||
@ -197,6 +334,10 @@ export default defineComponent({
|
||||
// return false;
|
||||
// };
|
||||
|
||||
const segmentChange = (e: CustomEvent) => {
|
||||
currentSegment.value = e.detail.value;
|
||||
};
|
||||
|
||||
onIonViewWillEnter(async () => {
|
||||
try {
|
||||
selectedNetwork.value = await getSelectedNetwork();
|
||||
@ -210,6 +351,24 @@ export default defineComponent({
|
||||
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 () => {
|
||||
alertTitle.value = "Error";
|
||||
if (
|
||||
@ -272,7 +431,97 @@ export default defineComponent({
|
||||
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({
|
||||
component: SelectedContacts,
|
||||
componentProps: {},
|
||||
@ -282,7 +531,11 @@ export default defineComponent({
|
||||
|
||||
const { data, role } = await modal.onWillDismiss();
|
||||
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,
|
||||
selectedNetwork,
|
||||
openModalAddContact,
|
||||
segmentChange,
|
||||
currentSegment,
|
||||
erc20,
|
||||
promptTransactionERC20,
|
||||
balanceOfERC20,
|
||||
currentBalanceERC20,
|
||||
wait,
|
||||
erc20Amount,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -15,8 +15,8 @@
|
||||
label="Error:"
|
||||
labelPlacement="stacked"
|
||||
style="overflow-y: scroll"
|
||||
:rows="10"
|
||||
:cols="20"
|
||||
:rows="18"
|
||||
:cols="70"
|
||||
:value="error"
|
||||
readonly
|
||||
></ion-textarea>
|
||||
|
@ -20,7 +20,11 @@
|
||||
<ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button>
|
||||
</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
|
||||
label="Contract Address(*)"
|
||||
label-placement="stacked"
|
||||
|
Loading…
Reference in New Issue
Block a user