Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial version #1

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.cache/
coverage/
dist/
node_modules/
33 changes: 33 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>el too</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="El Too — Dead simple multichain L2 stablecoin wallet">
<style>
body {
font-family: Helvetica Neue,Helvetica,Arial,sans-serif;
color: rgba(0, 0, 0, 0.8);
}
a {
color: rgba(0, 0, 0, 0.8);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
span.active {
border: 1px dotted black;
border-bottom: 1px solid white;
}
</style>
</head>

<body>
<div id="app"></div>
<script src="./src/index.tsx"></script>
</body>

</html>
38 changes: 38 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@leapdao/eltoo",
"version": "1.0.0",
"main": "index.js",
"author": "Kosta Korenkov <[email protected]>",
"license": "MIT",
"scripts": {
"start": "parcel index.html",
"build": "rm -rf dist/ && parcel build index.html --public-url / && size-limit",
"test": "jest src/ --coverage"
},
"size-limit": [
{
"path": "dist/src.*.js",
"limit": "200KB"
}
],
"dependencies": {
"ethers": "^5.4.6",
"preact": "^10.4.7"
},
"devDependencies": {
"@size-limit/preset-app": "^4.5.7",
"@types/jest": "^26.0.10",
"jest": "^26.4.2",
"parcel-bundler": "^1.12.4",
"size-limit": "^4.5.7",
"ts-jest": "^26.2.0",
"typescript": "^4.0.2"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
]
}
}
135 changes: 135 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Fragment, render, h } from 'preact';
import { Contract, ethers } from "ethers";
import { useMemo, useState, useEffect, useCallback } from 'preact/hooks';
import { NetworkConfig } from './types';

import useMultichainWallet, { ExtendedNetworkConfig } from './useMultichainWallet';
import useLocalWallet from './useLocalWallet';





const networks: NetworkConfig[] = [
{
chainId: 69,
name: 'Optimism Testnet',
rpcUrl: 'https://kovan.optimism.io',
tokenAddr: "0x3b8e53B3aB8E01Fb57D0c9E893bC4d655AA67d84", // USDC
},
{
chainId: 77,
name: 'XDAI Testnet (Sokol)',
rpcUrl: 'https://sokol.poa.network',
tokenAddr: "0x3b0977b9e563F63F219019616BBD12cB1cdFF527", // USDC
},
// {
// chainId: 80001,
// name: 'Polygon testnet',
// rpcUrl: 'https://matic-mainnet.chainstacklabs.com',
// tokenAddr: "0x3b8e53B3aB8E01Fb57D0c9E893bC4d655AA67d84", // USDC
// }
];

const modes = ['receive', 'send', 'view'];
type Mode = typeof modes[number];

type SendProps = {
address?: string;
amount?: bigint;
advanced?: boolean;
}

const Send = ({ address, amount, advanced }: SendProps) => {
const [targetAddress, setTargetAddress] = useState<string>(address || '');
const [targetAmount, setTargetAmount] = useState<bigint>(amount || BigInt(0));
const [supportedChains, setSupportedChains] = useState<number[]>([]);

useEffect(() => {
const urlParts = window.location.pathname.split("/");
if (urlParts[2]) setSupportedChains(urlParts[2].split(',').map(c => parseInt(c)));
if (urlParts[3]) setTargetAddress(urlParts[3]);
if (urlParts[4]) setTargetAmount(BigInt(urlParts[4]));
}, [window.location]);

const supportedChainNames = networks
.filter(n => supportedChains.includes(n.chainId))
.map(n => n.name);

return <div style={{ marginTop: '15px', padding: '25px', border: '1px dotted black' }}>
<div style={{ padding: '15px 0' }}>
Send{' '}
<input id="amount" type="text" value={ethers.utils.formatUnits(targetAmount, 6)} style={{
width: '50px'
}}/> USDC
to{' '}
<input id="address" type="text" value={targetAddress} style={{
width: '350px'
}} />

{advanced && <div>
<label>Supported chains: </label>
<span>{supportedChainNames.join(', ')}</span>
</div>}
</div>
<button>Send</button>
</div>
}

const App = () => {
const { wallet } = useLocalWallet();
const account = wallet.address;

const { balanceStr, networksWithWallet } = useMultichainWallet({ account, networks });
const [advanced, setAdvanced] = useState<boolean>(false);

const [mode, setMode] = useState<Mode>('view');

const supportedChains = networks.map(n => n.chainId);
const recieveUrl = `http://localhost:1234/send/${supportedChains.join(',')}/${wallet.address}`;

useEffect(() => {
const urlParts = window.location.pathname.split("/");
if (urlParts.length > 1 && modes.includes(urlParts[1])) {
setMode(urlParts[1]);
}
}, [window.location]);

return (
<main style={{ }}>
<header style={{ position: 'fixed', display: 'flex' }}>
<div>
<span style={{ fontSize: '48px' }}>${balanceStr}</span>
<div style={{ marginTop: '15px', gap: '30px', display: 'flex' }}>
<button onClick={() => setMode('receive')}>Receive</button>
<button onClick={() => setMode('send')}>Send</button>
</div>
</div>
<div>
<ul style={{ visibility: (advanced ? 'visible' : 'hidden') }}>
{networksWithWallet.map(({ name, wallet }: ExtendedNetworkConfig) => <li>
{name} — ${wallet.balanceStr}
</li>)}
</ul>
</div>
</header>

<div style={{ position: 'absolute', width: '100%', height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
{mode === 'receive' && <div style={{ padding: '30px' }}>
<a href={recieveUrl}>{recieveUrl}</a>
</div>}

{mode === 'send' && <Send advanced={advanced} />}
</div>

<div style={{ position: 'absolute', top: 0, right: 0, padding: '15px' }}>
<label>
advanced
<input type="checkbox" checked={advanced} onClick={() => setAdvanced(!advanced)} />
</label>
</div>
</main>
);
};

render(<App />, document.getElementById('app'))
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type NetworkConfig = {
chainId: number;
name: string;
rpcUrl: string;
tokenAddr: string;
};
17 changes: 17 additions & 0 deletions src/useLocalWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ethers } from "ethers";

export default () => {
let privateKey = localStorage.getItem('eltoo-private-key');
if (!privateKey) {
const wallet = ethers.Wallet.createRandom();
privateKey = wallet.privateKey;
localStorage.setItem('eltoo-private-key', privateKey);
}

const wallet = new ethers.Wallet(privateKey);

return {
wallet,
privateKey,
};
}
46 changes: 46 additions & 0 deletions src/useMultichainWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ethers } from "ethers";
import { useMemo } from "preact/hooks";
import { NetworkConfig } from "./types";
import useNetwork, { NetworkParams } from "./useNetwork";
import formatUsdc from "./utils/formatUsdc";

type UseMultichainWallet = {
account: string;
networks: NetworkConfig[];
};

export type ExtendedNetworkConfig = NetworkConfig & {
wallet: NetworkParams;
};

type MultichainParams = {
balance: bigint;
balanceStr: string;
networksWithWallet: ExtendedNetworkConfig[];
};

export default ({ account, networks }: UseMultichainWallet): MultichainParams => {
const networksWithWallet = networks.map(network => ({
...network,
wallet: useNetwork({ account, network })
}));

const balance = networksWithWallet.reduce(
(balance, network) => balance + BigInt(network.wallet.balance),
BigInt(0)
);

const balanceStr = useMemo(
() => {
if (!balance) return '0';
return formatUsdc(balance, networksWithWallet[0].wallet.tokenDecimals);
},
[balance, networksWithWallet]
);

return {
balance,
balanceStr,
networksWithWallet,
}
};
82 changes: 82 additions & 0 deletions src/useNetwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Contract, ethers } from "ethers";
import { useMemo, useState, useEffect } from 'preact/hooks';
import { NetworkConfig } from './types';
import formatUsdc from "./utils/formatUsdc";

type UseNetworkProps = {
account: string;
network: NetworkConfig;
};

export type NetworkParams = {
balance: bigint;
balanceStr: string;
tokenContract?: Contract;
tokenDecimals: number;
provider: ethers.providers.JsonRpcProvider;
};

const erc20abi = [
// Some details about the token
"function name() view returns (string)",
"function symbol() view returns (string)",

"function decimals() view returns (uint8)",

// Get the account balance
"function balanceOf(address) view returns (uint)",

// Send some of your tokens to someone else
"function transfer(address to, uint amount)",

// An event triggered whenever anyone transfers to someone else
"event Transfer(address indexed from, address indexed to, uint amount)"
];


export default ({ account, network }: UseNetworkProps): NetworkParams => {
const [balance, setBalance] = useState<bigint>(BigInt(0));
const [tokenDecimals, setTokenDecimals] = useState<number>(0);
const provider = useMemo(
() => new ethers.providers.JsonRpcProvider(network.rpcUrl),
[]
);

const tokenContract = useMemo(
() => {
if (!provider || !network) return;
return new ethers.Contract(network.tokenAddr, erc20abi, provider);
},
[network, provider]
);

useEffect(() => {
if (!tokenContract) return;
tokenContract.decimals().then(setTokenDecimals);
}, [tokenContract]);

useEffect(() => {
if (!tokenContract) return;
tokenContract.balanceOf(account).then(
(balance: ethers.BigNumber) => {
setBalance(BigInt(String(balance)));
}
);
}, [account, tokenContract]);

const balanceStr = useMemo(
() => {
if (!balance || !tokenDecimals) return '0';
return formatUsdc(balance, tokenDecimals);
},
[balance, tokenDecimals]
);

return {
provider,
tokenContract,
balance,
balanceStr,
tokenDecimals,
}
};
1 change: 1 addition & 0 deletions src/utils/erc20abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export
4 changes: 4 additions & 0 deletions src/utils/formatUsdc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ethers } from "ethers";

export default (value: bigint, decimals = 6) =>
parseFloat(ethers.utils.formatUnits(value, decimals)).toFixed(2);
Loading