Skip to content

Commit

Permalink
Merge pull request #16 from Patronum-Labs/ERC777
Browse files Browse the repository at this point in the history
Add support for ERC777
  • Loading branch information
YamenMerhi authored Jul 15, 2024
2 parents 2543f3f + 2975871 commit 8af2c70
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 1 deletion.
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ declare module '@vue/runtime-core' {
Erc20: typeof import('./src/tools/erc-20/erc-20.vue')['default']
Erc721: typeof import('./src/tools/erc-721/erc-721.vue')['default']
Erc725: typeof import('./src/tools/erc-725/erc-725.vue')['default']
Erc777: typeof import('./src/tools/erc-777/erc-777.vue')['default']
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
EvmAddress: typeof import('./src/tools/evm-address/evm-address.vue')['default']
EvmChecksum: typeof import('./src/tools/evm-checksum/evm-checksum.vue')['default']
Expand Down
352 changes: 352 additions & 0 deletions src/tools/erc-777/erc-777.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useThemeVars } from 'naive-ui';
import { ethers } from 'ethers';
// Constants
const ERC777_ABI = [
// ERC20 functions
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'function allowance(address,address) view returns (uint256)',
// ERC777 specific functions
'function granularity() view returns (uint256)',
'function defaultOperators() view returns (address[])',
'function isOperatorFor(address operator, address holder) view returns (bool)',
];
// Theme
const themeVars = useThemeVars();
// State
const rpcInput = ref('');
const contractAddressInput = ref('');
const errorMessage = ref('');
const walletConnected = ref(false);
const networkInfo = ref('');
const isEOA = ref(false);
const tokenName = ref('');
const tokenSymbol = ref('');
const tokenDecimals = ref('');
const tokenTotalSupply = ref('');
const tokenGranularity = ref('');
const tokenDefaultOperators = ref<string[]>([]);
const balanceAddress = ref('');
const balanceResult = ref('');
const allowanceOwner = ref('');
const allowanceSpender = ref('');
const allowanceResult = ref('');
const operatorAddress = ref('');
const holderAddress = ref('');
const isOperatorForResult = ref('');
// Providers
let provider: ethers.BrowserProvider | ethers.JsonRpcProvider | null = null;
// Computed properties
const contractAddressValid = computed(() => isValidAddress(contractAddressInput.value));
const rpcValid = computed(
() => walletConnected.value || (rpcInput.value.trim() !== '' && rpcInput.value.startsWith('http')),
);
const canQuery = computed(() => contractAddressValid.value && rpcValid.value && !isEOA.value);
// Helper functions
function isValidAddress(address: string): boolean {
return address.startsWith('0x') && address.length === 42;
}
function resetStatus() {
errorMessage.value = '';
isEOA.value = false;
tokenName.value = '';
tokenSymbol.value = '';
tokenDecimals.value = '';
tokenTotalSupply.value = '';
tokenGranularity.value = '';
tokenDefaultOperators.value = [];
balanceResult.value = '';
allowanceResult.value = '';
isOperatorForResult.value = '';
}
// Wallet connection
async function connectWallet() {
if (window.ethereum) {
try {
provider = new ethers.BrowserProvider(window.ethereum);
const network = await provider.getNetwork();
networkInfo.value = `${network.name} (Chain ID: ${network.chainId})`;
walletConnected.value = true;
rpcInput.value = 'Connected to Wallet RPC';
if (contractAddressValid.value) {
await checkIfEOA();
}
}
catch (error) {
errorMessage.value = 'Failed to connect to the wallet';
console.error(error);
}
}
else {
errorMessage.value = 'No Ethereum provider found. Please install MetaMask.';
}
}
async function disconnectWallet() {
provider = null;
walletConnected.value = false;
rpcInput.value = '';
networkInfo.value = '';
resetStatus();
}
async function checkIfEOA() {
if (!provider || !contractAddressValid.value) {
return;
}
try {
const code = await provider.getCode(contractAddressInput.value);
isEOA.value = code === '0x';
}
catch (error) {
console.error('Error checking if address is EOA:', error);
isEOA.value = false;
}
}
// Main functionality
async function queryTokenInfo() {
resetStatus();
if (!canQuery.value) {
errorMessage.value = 'Invalid input or no RPC/provider available';
return;
}
try {
if (!provider) {
provider = new ethers.JsonRpcProvider(rpcInput.value);
}
const contract = new ethers.Contract(contractAddressInput.value, ERC777_ABI, provider);
tokenName.value = await contract.name();
tokenSymbol.value = await contract.symbol();
tokenDecimals.value = await contract.decimals();
const totalSupply = await contract.totalSupply();
tokenTotalSupply.value = ethers.formatUnits(totalSupply, tokenDecimals.value);
tokenGranularity.value = await contract.granularity();
tokenDefaultOperators.value = await contract.defaultOperators();
}
catch (error) {
errorMessage.value = 'Error: Failed to query token info';
console.error(error);
}
}
async function queryBalance() {
if (!isValidAddress(balanceAddress.value)) {
errorMessage.value = 'Invalid address for balance query';
return;
}
try {
const contract = new ethers.Contract(contractAddressInput.value, ERC777_ABI, provider);
const balance = await contract.balanceOf(balanceAddress.value);
balanceResult.value = ethers.formatUnits(balance, tokenDecimals.value);
}
catch (error) {
errorMessage.value = 'Error: Failed to query balance';
console.error(error);
}
}
async function queryAllowance() {
if (!isValidAddress(allowanceOwner.value) || !isValidAddress(allowanceSpender.value)) {
errorMessage.value = 'Invalid address(es) for allowance query';
return;
}
try {
const contract = new ethers.Contract(contractAddressInput.value, ERC777_ABI, provider);
const allowance = await contract.allowance(allowanceOwner.value, allowanceSpender.value);
allowanceResult.value = ethers.formatUnits(allowance, tokenDecimals.value);
}
catch (error) {
errorMessage.value = 'Error: Failed to query allowance';
console.error(error);
}
}
async function queryIsOperatorFor() {
if (!isValidAddress(operatorAddress.value) || !isValidAddress(holderAddress.value)) {
errorMessage.value = 'Invalid address(es) for isOperatorFor query';
return;
}
try {
const contract = new ethers.Contract(contractAddressInput.value, ERC777_ABI, provider);
const isOperator = await contract.isOperatorFor(operatorAddress.value, holderAddress.value);
isOperatorForResult.value = isOperator.toString();
}
catch (error) {
errorMessage.value = 'Error: Failed to query isOperatorFor';
console.error(error);
}
}
// Watch for changes in the contract address
watch(contractAddressInput, async (newValue) => {
resetStatus();
if (isValidAddress(newValue) && provider) {
await checkIfEOA();
}
});
// Watch for changes in RPC input
watch(rpcInput, async () => {
resetStatus();
if (!walletConnected.value && rpcValid.value) {
try {
provider = new ethers.JsonRpcProvider(rpcInput.value);
const network = await provider.getNetwork();
networkInfo.value = `${network.name} (Chain ID: ${network.chainId})`;
walletConnected.value = true;
if (contractAddressValid.value) {
await checkIfEOA();
}
}
catch (error) {
console.error('Error creating provider:', error);
provider = null;
}
}
});
</script>

<template>
<c-card title="ERC777 Token Query">
<div align-center mb-2 flex>
<c-input-text
v-model:value="rpcInput"
:placeholder="walletConnected ? 'Connected to Wallet RPC' : 'RPC of the network...'"
:readonly="walletConnected"
label="Network: "
label-position="left"
label-align="right"
label-width="100px"
/>
<c-button @click="walletConnected ? disconnectWallet() : connectWallet()">
{{ walletConnected ? 'Disconnect' : 'Connect Wallet' }}
</c-button>
</div>

<div v-if="walletConnected" mb-2>
<span>Connected Network: {{ networkInfo }}</span>
</div>

<c-input-text
v-model:value="contractAddressInput"
placeholder="Contract address..."
label="Contract: "
label-position="left"
label-align="right"
label-width="100px"
mb-3
/>
<div v-if="isEOA" class="error-message" mb-2>
This address is an Externally Owned Account (EOA), not a contract.
</div>

<div class="button-container" mb-3>
<c-button :disabled="!canQuery" @click="queryTokenInfo">
Query Token Info
</c-button>
</div>

<c-input-text v-model:value="tokenName" label="Name: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />
<c-input-text v-model:value="tokenSymbol" label="Symbol: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />
<c-input-text v-model:value="tokenDecimals" label="Decimals: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />
<c-input-text v-model:value="tokenTotalSupply" label="Total Supply: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />
<c-input-text v-model:value="tokenGranularity" label="Granularity: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />
<c-input-text :value="tokenDefaultOperators.join(', ')" label="Default Operators: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />

<div class="separator" mb-3 />

<div class="input-container" mb-2>
<c-input-text
v-model:value="balanceAddress"
placeholder="Address to check balance..."
label="Balance Of: "
label-position="left"
label-align="right"
label-width="100px"
/>
<c-button :disabled="!canQuery" @click="queryBalance">
Query
</c-button>
</div>

<c-input-text v-model:value="balanceResult" label="Balance: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />

<div class="separator" mb-3 />

<c-input-text v-model:value="allowanceOwner" label="Owner: " label-position="left" label-align="right" label-width="100px" placeholder="Owner address..." mb-2 />
<c-input-text v-model:value="allowanceSpender" label="Spender: " label-position="left" label-align="right" label-width="100px" placeholder="Spender address..." mb-2 />

<div class="button-container" mb-2>
<c-button :disabled="!canQuery" @click="queryAllowance">
Query Allowance
</c-button>
</div>

<c-input-text v-model:value="allowanceResult" label="Allowance: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />

<div class="separator" mb-3 />

<c-input-text v-model:value="operatorAddress" label="Operator: " label-position="left" label-align="right" label-width="100px" placeholder="Operator address..." mb-2 />
<c-input-text v-model:value="holderAddress" label="Holder: " label-position="left" label-align="right" label-width="100px" placeholder="Holder address..." mb-2 />

<div class="button-container" mb-2>
<c-button :disabled="!canQuery" @click="queryIsOperatorFor">
Query Is Operator For
</c-button>
</div>

<c-input-text v-model:value="isOperatorForResult" label="Is Operator: " label-position="left" label-align="right" label-width="100px" readonly mb-2 />

<div v-if="errorMessage" class="error-message" mt-2>
{{ errorMessage }}
</div>
</c-card>
</template>

<style lang="less" scoped>
.button-container {
display: flex;
justify-content: center;
}
.input-container {
display: flex;
gap: 10px;
}
.error-message {
color: v-bind('themeVars.errorColor');
font-size: 0.9em;
}
.separator {
height: 1px;
background-color: #e0e0e0;
margin: 15px 0;
}
</style>
12 changes: 12 additions & 0 deletions src/tools/erc-777/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ArrowsShuffle } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'ERC777',
path: '/erc-777',
description: 'Checks the ERC777 standard common interface functions, including balance checks, info, and approvals.',
keywords: ['ERC777', 'Fungible Token', 'Token'],
component: () => import('./erc-777.vue'),
icon: ArrowsShuffle,
createdAt: new Date('2024-07-16'),
});
3 changes: 2 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { tool as erc165 } from './erc-165';
import { tool as erc173 } from './erc-173';
import { tool as erc191 } from './erc-191';
import { tool as erc721 } from './erc-721';
import { tool as erc777 } from './erc-777';
import { tool as erc725 } from './erc-725';
import { tool as erc1155 } from './erc-1155';
import { tool as erc1167 } from './erc-1167';
Expand All @@ -108,7 +109,7 @@ import { tool as evmNamehash } from './evm-namehash';
export const toolsByCategory: ToolCategory[] = [
{
name: 'ERC',
components: [erc20, erc165, erc173, erc191, erc721, erc725, erc1155, erc1167, erc1271],
components: [erc20, erc165, erc173, erc191, erc721, erc725, erc777, erc1155, erc1167, erc1271],
},
{
name: 'EVM',
Expand Down

0 comments on commit 8af2c70

Please sign in to comment.