[oracle-swap] Improve oracle-swap frontend (#544)
* stuff * make this look better * improving ux * basic functionality works * clean up appearance * 3 seconds * clean up code a bit * blah * change website title and favicon and fix useEffect warning * actually change website title and fix useEffect warning * update metadata * trigger deployment --------- Co-authored-by: Daniel Chew <cctdaniel@outlook.com>
This commit is contained in:
parent
4c5d0d5e1b
commit
3b44d02828
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
|
@ -7,7 +7,7 @@
|
|||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="Example oracle AMM application using Pyth price feeds."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
|
@ -24,7 +24,17 @@
|
|||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Pyth Example Oracle AMM</title>
|
||||
<script>
|
||||
const faviconTag = document.querySelector("link[rel~='icon']");
|
||||
const isDark = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const changeFavicon = () => {
|
||||
if (isDark.matches) faviconTag.href = "/favicon-light.ico";
|
||||
else faviconTag.href = "/favicon.ico";
|
||||
};
|
||||
changeFavicon();
|
||||
setInterval(changeFavicon, 1000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
@ -1,38 +1,112 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
.control-panel {
|
||||
flex: 1;
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
max-width: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
justify-content: flex-start;
|
||||
color: white;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
.tab-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
background-color: #ddd;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background-color: #fff;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
top: 5px; /* adjust as needed */
|
||||
left: 30px; /* adjust as needed */
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease-in-out;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.icon-container:hover .tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.exchange-rate {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
p,
|
||||
input {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 30px 0px 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 5px 0px 6px 0px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 3px;
|
||||
margin: 10px 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.swap-steps {
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0px 5px 0px 5px;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
import {
|
||||
Price,
|
||||
PriceFeed,
|
||||
EvmPriceServiceConnection,
|
||||
HexString,
|
||||
Price,
|
||||
PriceFeed,
|
||||
} from "@pythnetwork/pyth-evm-js";
|
||||
import IPythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
|
||||
import OracleSwapAbi from "./OracleSwapAbi.json";
|
||||
import ERC20Abi from "./ERC20Abi.json";
|
||||
import { useMetaMask } from "metamask-react";
|
||||
import Web3 from "web3";
|
||||
import { BigNumber } from "ethers";
|
||||
import { ChainState, ExchangeRateMeta, tokenQtyToNumber } from "./utils";
|
||||
import { OrderEntry } from "./OrderEntry";
|
||||
import { PriceText } from "./PriceText";
|
||||
import { MintButton } from "./MintButton";
|
||||
import { getBalance } from "./erc20";
|
||||
|
||||
const CONFIG = {
|
||||
// Each token is configured with its ERC20 contract address and Pyth Price Feed ID.
|
||||
|
@ -19,170 +20,124 @@ const CONFIG = {
|
|||
// Note that feeds have different ids on testnet / mainnet.
|
||||
baseToken: {
|
||||
name: "BRL",
|
||||
erc20Address: "0x8e2a09b54fF35Cc4fe3e7dba68bF4173cC559C69",
|
||||
erc20Address: "0xB3a2EDFEFC35afE110F983E32Eb67E671501de1f",
|
||||
pythPriceFeedId:
|
||||
"08f781a893bc9340140c5f89c8a96f438bcfae4d1474cc0f688e3a52892c7318",
|
||||
decimals: 18,
|
||||
},
|
||||
quoteToken: {
|
||||
name: "USDC",
|
||||
erc20Address: "0x98cDc14fe999435F3d4C2E65eC8863e0d70493Df",
|
||||
name: "USD",
|
||||
erc20Address: "0x8C65F3b18fB29D756d26c1965d84DBC273487624",
|
||||
pythPriceFeedId:
|
||||
"41f3625971ca2ed2263e78573fe5ce23e13d2558ed3f2e47ab0f84fb9e7ae722",
|
||||
"1fc18861232290221461220bd4e2acd1dcdfbc89c84092c93c18bdc7756c1588",
|
||||
decimals: 18,
|
||||
},
|
||||
swapContractAddress: "0xf3161b2B32761B46C084a7e1d8993C19703C09e7",
|
||||
swapContractAddress: "0x15F9ccA28688F5E6Cbc8B00A8f33e8cE73eD7B02",
|
||||
pythContractAddress: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
|
||||
priceServiceUrl: "https://xc-testnet.pyth.network",
|
||||
mintQty: 100,
|
||||
};
|
||||
|
||||
// The Pyth price service client is used to retrieve the current Pyth prices and the price update data that
|
||||
// needs to be posted on-chain with each transaction.
|
||||
const pythPriceService = new EvmPriceServiceConnection(CONFIG.priceServiceUrl);
|
||||
|
||||
function timeAgo(diff: number) {
|
||||
if (diff > 60) {
|
||||
return ">1m";
|
||||
} else if (diff < 2) {
|
||||
return "<2s";
|
||||
} else {
|
||||
return `${diff.toFixed(0)}s`;
|
||||
}
|
||||
}
|
||||
|
||||
function PriceTicker(props: { price: Price | undefined; currentTime: Date }) {
|
||||
const price = props.price;
|
||||
|
||||
if (price === undefined) {
|
||||
return <span style={{ color: "grey" }}>loading...</span>;
|
||||
} else {
|
||||
const now = props.currentTime.getTime() / 1000;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span style={{ color: "green" }}>
|
||||
{" "}
|
||||
{price.getPriceAsNumberUnchecked().toFixed(3) +
|
||||
" ± " +
|
||||
price.getConfAsNumberUnchecked().toFixed(3)}{" "}
|
||||
</span>
|
||||
<span style={{ color: "grey" }}>
|
||||
last updated {timeAgo(now - price.publishTime)} ago
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// React component that shows the offchain price and confidence interval
|
||||
function PriceText(props: {
|
||||
price: Record<HexString, Price>;
|
||||
currentTime: Date;
|
||||
}) {
|
||||
let basePrice = props.price[CONFIG.baseToken.pythPriceFeedId];
|
||||
let quotePrice = props.price[CONFIG.quoteToken.pythPriceFeedId];
|
||||
|
||||
let exchangeRate: number | undefined = undefined;
|
||||
let lastUpdatedTime: Date | undefined = undefined;
|
||||
if (basePrice !== undefined && quotePrice !== undefined) {
|
||||
exchangeRate =
|
||||
basePrice.getPriceAsNumberUnchecked() /
|
||||
quotePrice.getPriceAsNumberUnchecked();
|
||||
lastUpdatedTime = new Date(
|
||||
Math.max(basePrice.publishTime, quotePrice.publishTime) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Current Exchange Rate:{" "}
|
||||
{exchangeRate !== undefined ? (
|
||||
exchangeRate.toFixed(4)
|
||||
) : (
|
||||
<span style={{ color: "grey" }}>"loading"</span>
|
||||
)}
|
||||
<br />
|
||||
Last updated at:{" "}
|
||||
{lastUpdatedTime !== undefined
|
||||
? lastUpdatedTime.toISOString()
|
||||
: "loading"}
|
||||
<br />
|
||||
<br />
|
||||
Pyth {CONFIG.baseToken.name} price:{" "}
|
||||
<PriceTicker price={basePrice} currentTime={props.currentTime} />
|
||||
<br />
|
||||
Pyth {CONFIG.quoteToken.name} price:{" "}
|
||||
<PriceTicker price={quotePrice} currentTime={props.currentTime} />
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PoolStatistics(props: { web3: Web3 | undefined }) {
|
||||
const [baseQty, setBaseQty] = useState<number>(0);
|
||||
const [quoteQty, setQuoteQty] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
async function queryQtys() {
|
||||
if (props.web3 !== undefined) {
|
||||
const swapContract = new props.web3.eth.Contract(
|
||||
OracleSwapAbi as any,
|
||||
CONFIG.swapContractAddress
|
||||
);
|
||||
|
||||
const baseQty =
|
||||
Number(await swapContract.methods.baseBalance().call()) / 1e18;
|
||||
const quoteQty =
|
||||
Number(await swapContract.methods.quoteBalance().call()) / 1e18;
|
||||
setBaseQty(baseQty);
|
||||
setQuoteQty(quoteQty);
|
||||
}
|
||||
}
|
||||
|
||||
const interval = setInterval(queryQtys, 5000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [props.web3]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Contract address: {CONFIG.swapContractAddress}</p>
|
||||
<p>
|
||||
Pool contains {baseQty} {CONFIG.baseToken.name} and {quoteQty}{" "}
|
||||
{CONFIG.quoteToken.name}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { status, connect, account, ethereum } = useMetaMask();
|
||||
|
||||
const [qty, setQty] = useState<string>("0");
|
||||
const [web3, setWeb3] = useState<Web3 | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "connected") {
|
||||
setWeb3(new Web3(ethereum));
|
||||
}
|
||||
}, [status]);
|
||||
}, [status, ethereum]);
|
||||
|
||||
const [chainState, setChainState] = useState<ChainState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function refreshChainState() {
|
||||
if (web3 !== undefined && account !== null) {
|
||||
setChainState({
|
||||
accountBaseBalance: await getBalance(
|
||||
web3,
|
||||
CONFIG.baseToken.erc20Address,
|
||||
account
|
||||
),
|
||||
accountQuoteBalance: await getBalance(
|
||||
web3,
|
||||
CONFIG.quoteToken.erc20Address,
|
||||
account
|
||||
),
|
||||
poolBaseBalance: await getBalance(
|
||||
web3,
|
||||
CONFIG.baseToken.erc20Address,
|
||||
CONFIG.swapContractAddress
|
||||
),
|
||||
poolQuoteBalance: await getBalance(
|
||||
web3,
|
||||
CONFIG.quoteToken.erc20Address,
|
||||
CONFIG.swapContractAddress
|
||||
),
|
||||
});
|
||||
} else {
|
||||
setChainState(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const interval = setInterval(refreshChainState, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [web3, account]);
|
||||
|
||||
const [pythOffChainPrice, setPythOffChainPrice] = useState<
|
||||
Record<HexString, Price>
|
||||
>({});
|
||||
|
||||
// Subscribe to offchain prices. These are the prices that a typical frontend will want to show.
|
||||
pythPriceService.subscribePriceFeedUpdates(
|
||||
[CONFIG.baseToken.pythPriceFeedId, CONFIG.quoteToken.pythPriceFeedId],
|
||||
(priceFeed: PriceFeed) => {
|
||||
const price = priceFeed.getPriceUnchecked(); // Fine to use unchecked (not checking for staleness) because this must be a recent price given that it comes from a websocket subscription.
|
||||
setPythOffChainPrice({
|
||||
...pythOffChainPrice,
|
||||
[priceFeed.id]: price,
|
||||
});
|
||||
useEffect(() => {
|
||||
// The Pyth price service client is used to retrieve the current Pyth prices and the price update data that
|
||||
// needs to be posted on-chain with each transaction.
|
||||
const pythPriceService = new EvmPriceServiceConnection(
|
||||
CONFIG.priceServiceUrl
|
||||
);
|
||||
|
||||
pythPriceService.subscribePriceFeedUpdates(
|
||||
[CONFIG.baseToken.pythPriceFeedId, CONFIG.quoteToken.pythPriceFeedId],
|
||||
(priceFeed: PriceFeed) => {
|
||||
const price = priceFeed.getPriceUnchecked(); // Fine to use unchecked (not checking for staleness) because this must be a recent price given that it comes from a websocket subscription.
|
||||
setPythOffChainPrice({
|
||||
...pythOffChainPrice,
|
||||
[priceFeed.id]: price,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
pythPriceService.closeWebSocket();
|
||||
};
|
||||
}, [pythOffChainPrice]);
|
||||
|
||||
const [exchangeRateMeta, setExchangeRateMeta] = useState<
|
||||
ExchangeRateMeta | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
let basePrice = pythOffChainPrice[CONFIG.baseToken.pythPriceFeedId];
|
||||
let quotePrice = pythOffChainPrice[CONFIG.quoteToken.pythPriceFeedId];
|
||||
|
||||
if (basePrice !== undefined && quotePrice !== undefined) {
|
||||
const exchangeRate =
|
||||
basePrice.getPriceAsNumberUnchecked() /
|
||||
quotePrice.getPriceAsNumberUnchecked();
|
||||
const lastUpdatedTime = new Date(
|
||||
Math.max(basePrice.publishTime, quotePrice.publishTime) * 1000
|
||||
);
|
||||
setExchangeRateMeta({ rate: exchangeRate, lastUpdatedTime });
|
||||
} else {
|
||||
setExchangeRateMeta(undefined);
|
||||
}
|
||||
);
|
||||
}, [pythOffChainPrice]);
|
||||
|
||||
const [time, setTime] = useState<Date>(new Date());
|
||||
|
||||
|
@ -193,130 +148,153 @@ function App() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const [isBuy, setIsBuy] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<div style={{ float: "right", border: "1px solid white" }}>
|
||||
<label>
|
||||
Your address: <br /> {account}
|
||||
</label>
|
||||
<button
|
||||
onClick={async () => {
|
||||
connect();
|
||||
}}
|
||||
disabled={status === "connected"}
|
||||
>
|
||||
{" "}
|
||||
Connect Wallet{" "}
|
||||
</button>
|
||||
</div>
|
||||
<div className="control-panel">
|
||||
<h3>Control Panel</h3>
|
||||
|
||||
<p>
|
||||
Swap between {CONFIG.baseToken.name} and {CONFIG.quoteToken.name}
|
||||
</p>
|
||||
<PriceText price={pythOffChainPrice} currentTime={time} />
|
||||
<div>
|
||||
<label>
|
||||
Order size:
|
||||
<input
|
||||
type="text"
|
||||
name="base"
|
||||
value={qty}
|
||||
onChange={(event) => {
|
||||
setQty(event.target.value);
|
||||
{status === "connected" ? (
|
||||
<label>
|
||||
Connected Wallet: <br /> {account}
|
||||
</label>
|
||||
) : (
|
||||
<button
|
||||
onClick={async () => {
|
||||
connect();
|
||||
}}
|
||||
/>
|
||||
{CONFIG.baseToken.name}
|
||||
</label>
|
||||
>
|
||||
{" "}
|
||||
Connect Wallet{" "}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await authorizeTokens(
|
||||
web3!,
|
||||
CONFIG.quoteToken.erc20Address,
|
||||
account!
|
||||
);
|
||||
await authorizeTokens(
|
||||
web3!,
|
||||
CONFIG.baseToken.erc20Address,
|
||||
account!
|
||||
);
|
||||
}}
|
||||
disabled={status !== "connected" || !pythOffChainPrice}
|
||||
>
|
||||
{" "}
|
||||
Authorize ERC20 Transfers{" "}
|
||||
</button>{" "}
|
||||
<button
|
||||
onClick={async () => {
|
||||
await sendSwapTx(web3!, account!, qty, true);
|
||||
}}
|
||||
disabled={status !== "connected" || !pythOffChainPrice}
|
||||
>
|
||||
{" "}
|
||||
Buy{" "}
|
||||
</button>{" "}
|
||||
<button
|
||||
onClick={async () => {
|
||||
await sendSwapTx(web3!, account!, qty, false);
|
||||
}}
|
||||
disabled={status !== "connected" || !pythOffChainPrice}
|
||||
>
|
||||
{" "}
|
||||
Sell{" "}
|
||||
</button>{" "}
|
||||
<h3>Wallet Balances</h3>
|
||||
{chainState !== undefined ? (
|
||||
<div>
|
||||
<p>
|
||||
{tokenQtyToNumber(
|
||||
chainState.accountBaseBalance,
|
||||
CONFIG.baseToken.decimals
|
||||
)}{" "}
|
||||
{CONFIG.baseToken.name}
|
||||
<MintButton
|
||||
web3={web3!}
|
||||
sender={account!}
|
||||
erc20Address={CONFIG.baseToken.erc20Address}
|
||||
destination={account!}
|
||||
qty={CONFIG.mintQty}
|
||||
decimals={CONFIG.baseToken.decimals}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{tokenQtyToNumber(
|
||||
chainState.accountQuoteBalance,
|
||||
CONFIG.quoteToken.decimals
|
||||
)}{" "}
|
||||
{CONFIG.quoteToken.name}
|
||||
<MintButton
|
||||
web3={web3!}
|
||||
sender={account!}
|
||||
erc20Address={CONFIG.quoteToken.erc20Address}
|
||||
destination={account!}
|
||||
qty={CONFIG.mintQty}
|
||||
decimals={CONFIG.quoteToken.decimals}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>loading...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PoolStatistics web3={web3} />
|
||||
</header>
|
||||
<h3>AMM Balances</h3>
|
||||
<div>
|
||||
<p>Contract address: {CONFIG.swapContractAddress}</p>
|
||||
{chainState !== undefined ? (
|
||||
<div>
|
||||
<p>
|
||||
{tokenQtyToNumber(
|
||||
chainState.poolBaseBalance,
|
||||
CONFIG.baseToken.decimals
|
||||
)}{" "}
|
||||
{CONFIG.baseToken.name}
|
||||
<MintButton
|
||||
web3={web3!}
|
||||
sender={account!}
|
||||
erc20Address={CONFIG.baseToken.erc20Address}
|
||||
destination={CONFIG.swapContractAddress}
|
||||
qty={CONFIG.mintQty}
|
||||
decimals={CONFIG.baseToken.decimals}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{tokenQtyToNumber(
|
||||
chainState.poolQuoteBalance,
|
||||
CONFIG.quoteToken.decimals
|
||||
)}{" "}
|
||||
{CONFIG.quoteToken.name}
|
||||
<MintButton
|
||||
web3={web3!}
|
||||
sender={account!}
|
||||
erc20Address={CONFIG.quoteToken.erc20Address}
|
||||
destination={CONFIG.swapContractAddress}
|
||||
qty={CONFIG.mintQty}
|
||||
decimals={CONFIG.quoteToken.decimals}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"main"}>
|
||||
<h3>
|
||||
Swap between {CONFIG.baseToken.name} and {CONFIG.quoteToken.name}
|
||||
</h3>
|
||||
<PriceText
|
||||
price={pythOffChainPrice}
|
||||
currentTime={time}
|
||||
rate={exchangeRateMeta}
|
||||
baseToken={CONFIG.baseToken}
|
||||
quoteToken={CONFIG.quoteToken}
|
||||
/>
|
||||
<div className="tab-header">
|
||||
<div
|
||||
className={`tab-item ${isBuy ? "active" : ""}`}
|
||||
onClick={() => setIsBuy(true)}
|
||||
>
|
||||
Buy
|
||||
</div>
|
||||
<div
|
||||
className={`tab-item ${!isBuy ? "active" : ""}`}
|
||||
onClick={() => setIsBuy(false)}
|
||||
>
|
||||
Sell
|
||||
</div>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
<OrderEntry
|
||||
web3={web3}
|
||||
account={account}
|
||||
isBuy={isBuy}
|
||||
approxPrice={exchangeRateMeta?.rate}
|
||||
baseToken={CONFIG.baseToken}
|
||||
quoteToken={CONFIG.quoteToken}
|
||||
priceServiceUrl={CONFIG.priceServiceUrl}
|
||||
pythContractAddress={CONFIG.pythContractAddress}
|
||||
swapContractAddress={CONFIG.swapContractAddress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function authorizeTokens(
|
||||
web3: Web3,
|
||||
erc20Address: string,
|
||||
sender: string
|
||||
) {
|
||||
const erc20 = new web3.eth.Contract(ERC20Abi as any, erc20Address);
|
||||
|
||||
await erc20.methods
|
||||
.approve(CONFIG.swapContractAddress, BigNumber.from("2").pow(256).sub(1))
|
||||
.send({ from: sender });
|
||||
}
|
||||
|
||||
async function sendSwapTx(
|
||||
web3: Web3,
|
||||
sender: string,
|
||||
qty: string,
|
||||
isBuy: boolean
|
||||
) {
|
||||
const priceFeedUpdateData = await pythPriceService.getPriceFeedsUpdateData([
|
||||
CONFIG.baseToken.pythPriceFeedId,
|
||||
CONFIG.quoteToken.pythPriceFeedId,
|
||||
]);
|
||||
|
||||
const pythContract = new web3.eth.Contract(
|
||||
IPythAbi as any,
|
||||
CONFIG.pythContractAddress
|
||||
);
|
||||
|
||||
const updateFee = await pythContract.methods
|
||||
.getUpdateFee(priceFeedUpdateData.length)
|
||||
.call();
|
||||
|
||||
const swapContract = new web3.eth.Contract(
|
||||
OracleSwapAbi as any,
|
||||
CONFIG.swapContractAddress
|
||||
);
|
||||
|
||||
// Note: this code assumes that the ERC20 token has 18 decimals. This may not be the case for arbitrary tokens.
|
||||
const qtyWei = BigNumber.from(qty).mul(BigNumber.from(10).pow(18));
|
||||
await swapContract.methods
|
||||
.swap(isBuy, qtyWei, priceFeedUpdateData)
|
||||
.send({ value: updateFee, from: sender });
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -1,272 +0,0 @@
|
|||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "version",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_extraData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "approveAndCall",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "remaining",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_initialAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_tokenName",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "_decimalUnits",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"name": "_tokenSymbol",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"payable": false,
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
import Web3 from "web3";
|
||||
import { numberToTokenQty } from "./utils";
|
||||
import { mint } from "./erc20";
|
||||
|
||||
export function MintButton(props: {
|
||||
web3: Web3;
|
||||
sender: string;
|
||||
erc20Address: string;
|
||||
destination: string;
|
||||
qty: number;
|
||||
decimals: number;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await mint(
|
||||
props.web3,
|
||||
props.sender,
|
||||
props.erc20Address,
|
||||
props.destination,
|
||||
numberToTokenQty(props.qty, props.decimals)
|
||||
);
|
||||
}}
|
||||
>
|
||||
Mint {props.qty}
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
import Web3 from "web3";
|
||||
import { BigNumber } from "ethers";
|
||||
import { TokenConfig, numberToTokenQty, tokenQtyToNumber } from "./utils";
|
||||
import IPythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
|
||||
import OracleSwapAbi from "./abi/OracleSwapAbi.json";
|
||||
import { approveToken, getApprovedQuantity } from "./erc20";
|
||||
import { EvmPriceServiceConnection } from "@pythnetwork/pyth-evm-js";
|
||||
|
||||
/**
|
||||
* The order entry component lets users enter a quantity of the base token to buy/sell and submit
|
||||
* the transaction to the blockchain.
|
||||
*/
|
||||
export function OrderEntry(props: {
|
||||
web3: Web3 | undefined;
|
||||
account: string | null;
|
||||
isBuy: boolean;
|
||||
approxPrice: number | undefined;
|
||||
baseToken: TokenConfig;
|
||||
quoteToken: TokenConfig;
|
||||
priceServiceUrl: string;
|
||||
pythContractAddress: string;
|
||||
swapContractAddress: string;
|
||||
}) {
|
||||
const [qty, setQty] = useState<string>("1");
|
||||
const [qtyBn, setQtyBn] = useState<BigNumber | undefined>(
|
||||
BigNumber.from("1")
|
||||
);
|
||||
const [authorizedQty, setAuthorizedQty] = useState<BigNumber>(
|
||||
BigNumber.from("0")
|
||||
);
|
||||
const [isAuthorized, setIsAuthorized] = useState<boolean>(false);
|
||||
|
||||
const [spentToken, setSpentToken] = useState<TokenConfig>(props.baseToken);
|
||||
const [approxQuoteSize, setApproxQuoteSize] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.isBuy) {
|
||||
setSpentToken(props.quoteToken);
|
||||
} else {
|
||||
setSpentToken(props.baseToken);
|
||||
}
|
||||
}, [props.isBuy]);
|
||||
|
||||
useEffect(() => {
|
||||
async function helper() {
|
||||
if (props.web3 !== undefined && props.account !== null) {
|
||||
setAuthorizedQty(
|
||||
await getApprovedQuantity(
|
||||
props.web3!,
|
||||
spentToken.erc20Address,
|
||||
props.account!,
|
||||
props.swapContractAddress
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setAuthorizedQty(BigNumber.from("0"));
|
||||
}
|
||||
}
|
||||
|
||||
helper();
|
||||
const interval = setInterval(helper, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [props.web3, props.account, spentToken]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const qtyBn = numberToTokenQty(qty, props.baseToken.decimals);
|
||||
setQtyBn(qtyBn);
|
||||
} catch (error) {
|
||||
setQtyBn(undefined);
|
||||
}
|
||||
}, [qty]);
|
||||
|
||||
useEffect(() => {
|
||||
if (qtyBn !== undefined) {
|
||||
setIsAuthorized(authorizedQty.gte(qtyBn));
|
||||
} else {
|
||||
setIsAuthorized(false);
|
||||
}
|
||||
}, [qtyBn, authorizedQty]);
|
||||
|
||||
useEffect(() => {
|
||||
if (qtyBn !== undefined && props.approxPrice !== undefined) {
|
||||
setApproxQuoteSize(
|
||||
tokenQtyToNumber(qtyBn, props.baseToken.decimals) * props.approxPrice
|
||||
);
|
||||
} else {
|
||||
setApproxQuoteSize(undefined);
|
||||
}
|
||||
}, [props.approxPrice, qtyBn]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
{props.isBuy ? "Buy" : "Sell"}
|
||||
<input
|
||||
type="text"
|
||||
name="base"
|
||||
value={qty}
|
||||
onChange={(event) => {
|
||||
setQty(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{props.baseToken.name}
|
||||
</p>
|
||||
{qtyBn !== undefined && approxQuoteSize !== undefined ? (
|
||||
props.isBuy ? (
|
||||
<p>
|
||||
Pay {approxQuoteSize.toFixed(3)} {props.quoteToken.name} to
|
||||
receive{" "}
|
||||
{tokenQtyToNumber(qtyBn, props.baseToken.decimals).toFixed(3)}{" "}
|
||||
{props.baseToken.name}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Pay {tokenQtyToNumber(qtyBn, props.baseToken.decimals).toFixed(3)}{" "}
|
||||
{props.baseToken.name} to receive {approxQuoteSize.toFixed(3)}{" "}
|
||||
{props.quoteToken.name}
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<p>Transaction details are loading...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={"swap-steps"}>
|
||||
{props.account === null || props.web3 === undefined ? (
|
||||
<div>Connect your wallet to swap</div>
|
||||
) : (
|
||||
<div>
|
||||
1.{" "}
|
||||
<button
|
||||
onClick={async () => {
|
||||
await approveToken(
|
||||
props.web3!,
|
||||
spentToken.erc20Address,
|
||||
props.account!,
|
||||
props.swapContractAddress
|
||||
);
|
||||
}}
|
||||
disabled={isAuthorized}
|
||||
>
|
||||
{" "}
|
||||
Approve{" "}
|
||||
</button>
|
||||
2.
|
||||
<button
|
||||
onClick={async () => {
|
||||
await sendSwapTx(
|
||||
props.web3!,
|
||||
props.priceServiceUrl,
|
||||
props.baseToken.pythPriceFeedId,
|
||||
props.quoteToken.pythPriceFeedId,
|
||||
props.pythContractAddress,
|
||||
props.swapContractAddress,
|
||||
props.account!,
|
||||
qtyBn!,
|
||||
props.isBuy
|
||||
);
|
||||
}}
|
||||
disabled={!isAuthorized}
|
||||
>
|
||||
{" "}
|
||||
Submit{" "}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function sendSwapTx(
|
||||
web3: Web3,
|
||||
priceServiceUrl: string,
|
||||
baseTokenPriceFeedId: string,
|
||||
quoteTokenPriceFeedId: string,
|
||||
pythContractAddress: string,
|
||||
swapContractAddress: string,
|
||||
sender: string,
|
||||
qtyWei: BigNumber,
|
||||
isBuy: boolean
|
||||
) {
|
||||
const pythPriceService = new EvmPriceServiceConnection(priceServiceUrl);
|
||||
const priceFeedUpdateData = await pythPriceService.getPriceFeedsUpdateData([
|
||||
baseTokenPriceFeedId,
|
||||
quoteTokenPriceFeedId,
|
||||
]);
|
||||
|
||||
const pythContract = new web3.eth.Contract(
|
||||
IPythAbi as any,
|
||||
pythContractAddress
|
||||
);
|
||||
|
||||
const updateFee = await pythContract.methods
|
||||
.getUpdateFee(priceFeedUpdateData.length)
|
||||
.call();
|
||||
|
||||
const swapContract = new web3.eth.Contract(
|
||||
OracleSwapAbi as any,
|
||||
swapContractAddress
|
||||
);
|
||||
|
||||
await swapContract.methods
|
||||
.swap(isBuy, qtyWei, priceFeedUpdateData)
|
||||
.send({ value: updateFee, from: sender });
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import { useState } from "react";
|
||||
import { HexString, Price } from "@pythnetwork/pyth-evm-js";
|
||||
import { ExchangeRateMeta, timeAgo, TokenConfig } from "./utils";
|
||||
|
||||
export function PriceTicker(props: {
|
||||
price: Price | undefined;
|
||||
currentTime: Date;
|
||||
tokenName: string;
|
||||
}) {
|
||||
const price = props.price;
|
||||
|
||||
if (price === undefined) {
|
||||
return <span style={{ color: "grey" }}>loading...</span>;
|
||||
} else {
|
||||
const now = props.currentTime.getTime() / 1000;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Pyth {props.tokenName} price:{" "}
|
||||
<span style={{ color: "green" }}>
|
||||
{" "}
|
||||
{price.getPriceAsNumberUnchecked().toFixed(3) +
|
||||
" ± " +
|
||||
price.getConfAsNumberUnchecked().toFixed(3)}{" "}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span style={{ color: "grey" }}>
|
||||
last updated {timeAgo(now - price.publishTime)} ago
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the current exchange rate with a tooltip for pyth prices.
|
||||
*/
|
||||
export function PriceText(props: {
|
||||
rate: ExchangeRateMeta | undefined;
|
||||
price: Record<HexString, Price>;
|
||||
currentTime: Date;
|
||||
baseToken: TokenConfig;
|
||||
quoteToken: TokenConfig;
|
||||
}) {
|
||||
let basePrice = props.price[props.baseToken.pythPriceFeedId];
|
||||
let quotePrice = props.price[props.quoteToken.pythPriceFeedId];
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.rate !== undefined ? (
|
||||
<div>
|
||||
Current Exchange Rate:{" "}
|
||||
<span className={"exchange-rate"}>{props.rate.rate.toFixed(4)}</span>{" "}
|
||||
<span
|
||||
className="icon-container"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
(details)
|
||||
{showTooltip && (
|
||||
<div className="tooltip">
|
||||
<PriceTicker
|
||||
price={basePrice}
|
||||
currentTime={props.currentTime}
|
||||
tokenName={props.baseToken.name}
|
||||
/>
|
||||
<PriceTicker
|
||||
price={quotePrice}
|
||||
currentTime={props.currentTime}
|
||||
tokenName={props.quoteToken.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
<p className={"last-updated"}>
|
||||
Last updated{" "}
|
||||
{timeAgo(
|
||||
(props.currentTime.getTime() -
|
||||
props.rate.lastUpdatedTime.getTime()) /
|
||||
1000
|
||||
)}{" "}
|
||||
ago
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p>Exchange rate is loading...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "symbol",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "initialAccount",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "initialBalance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "payable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approveInternal",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "burn",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "subtractedValue",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "decreaseAllowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "addedValue",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "increaseAllowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "mint",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferInternal",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,178 @@
|
|||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_pyth",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_baseTokenPriceId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_quoteTokenPriceId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_baseToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_quoteToken",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "amountUsd",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "amountWei",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "baseBalance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "baseToken",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract ERC20",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "quoteBalance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "quoteToken",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract ERC20",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_baseTokenPriceId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_quoteTokenPriceId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_baseToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_quoteToken",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "reinitialize",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "isBuy",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "size",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes[]",
|
||||
"name": "pythUpdateData",
|
||||
"type": "bytes[]"
|
||||
}
|
||||
],
|
||||
"name": "swap",
|
||||
"outputs": [],
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "withdrawAll",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"stateMutability": "payable",
|
||||
"type": "receive"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,68 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import "./App.css";
|
||||
import {
|
||||
Price,
|
||||
PriceFeed,
|
||||
EvmPriceServiceConnection,
|
||||
HexString,
|
||||
} from "@pythnetwork/pyth-evm-js";
|
||||
import IPythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
|
||||
import OracleSwapAbi from "./abi/OracleSwapAbi.json";
|
||||
import ERC20Abi from "./abi/ERC20MockAbi.json";
|
||||
import { useMetaMask } from "metamask-react";
|
||||
import Web3 from "web3";
|
||||
import { BigNumber } from "ethers";
|
||||
import { TokenConfig } from "./utils";
|
||||
|
||||
/**
|
||||
* Allow `approvedSpender` to spend your
|
||||
* @param web3
|
||||
* @param erc20Address
|
||||
* @param sender
|
||||
* @param approvedSpender
|
||||
*/
|
||||
export async function approveToken(
|
||||
web3: Web3,
|
||||
erc20Address: string,
|
||||
sender: string,
|
||||
approvedSpender: string
|
||||
) {
|
||||
const erc20 = new web3.eth.Contract(ERC20Abi as any, erc20Address);
|
||||
|
||||
await erc20.methods
|
||||
.approve(approvedSpender, BigNumber.from("2").pow(256).sub(1))
|
||||
.send({ from: sender });
|
||||
}
|
||||
|
||||
export async function getApprovedQuantity(
|
||||
web3: Web3,
|
||||
erc20Address: string,
|
||||
sender: string,
|
||||
approvedSpender: string
|
||||
): Promise<BigNumber> {
|
||||
const erc20 = new web3.eth.Contract(ERC20Abi as any, erc20Address);
|
||||
let allowance = BigNumber.from(
|
||||
await erc20.methods.allowance(sender, approvedSpender).call()
|
||||
);
|
||||
return allowance as BigNumber;
|
||||
}
|
||||
|
||||
export async function getBalance(
|
||||
web3: Web3,
|
||||
erc20Address: string,
|
||||
address: string
|
||||
): Promise<BigNumber> {
|
||||
const erc20 = new web3.eth.Contract(ERC20Abi as any, erc20Address);
|
||||
return BigNumber.from(await erc20.methods.balanceOf(address).call());
|
||||
}
|
||||
|
||||
export async function mint(
|
||||
web3: Web3,
|
||||
sender: string,
|
||||
erc20Address: string,
|
||||
destinationAddress: string,
|
||||
quantity: BigNumber
|
||||
) {
|
||||
const erc20 = new web3.eth.Contract(ERC20Abi as any, erc20Address);
|
||||
await erc20.methods.mint(destinationAddress, quantity).send({ from: sender });
|
||||
}
|
|
@ -11,10 +11,3 @@ code {
|
|||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { BigNumber } from "ethers";
|
||||
|
||||
export interface TokenConfig {
|
||||
name: string;
|
||||
erc20Address: string;
|
||||
pythPriceFeedId: string;
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface ExchangeRateMeta {
|
||||
rate: number;
|
||||
lastUpdatedTime: Date;
|
||||
}
|
||||
|
||||
export interface ChainState {
|
||||
accountBaseBalance: BigNumber;
|
||||
accountQuoteBalance: BigNumber;
|
||||
poolBaseBalance: BigNumber;
|
||||
poolQuoteBalance: BigNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a string rendering of a time delta. `diff` is the difference between the current
|
||||
* time and previous time in seconds.
|
||||
*/
|
||||
export function timeAgo(diff: number): string {
|
||||
if (diff > 60) {
|
||||
return `${(diff / 60).toFixed(0)}m`;
|
||||
} else if (diff < 2) {
|
||||
return "<2s";
|
||||
} else {
|
||||
return `${diff.toFixed(0)}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hacky function for converting a floating point number into a token quantity that's useful for ETH or ERC-20 tokens.
|
||||
* Note: this function assumes that decimals >= 6 (which is pretty much always the case for tokens)
|
||||
*/
|
||||
export function numberToTokenQty(
|
||||
x: number | string,
|
||||
decimals: number
|
||||
): BigNumber {
|
||||
if (typeof x == "string") {
|
||||
x = Number.parseFloat(x);
|
||||
}
|
||||
return BigNumber.from(Math.floor(x * 1000000)).mul(
|
||||
BigNumber.from(10).pow(decimals - 6)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hacky function for converting a token quantity back into a floating point number.
|
||||
* Note: this function assumes that decimals >= 6 (which is pretty much always the case for tokens)
|
||||
*/
|
||||
export function tokenQtyToNumber(x: BigNumber, decimals: number): number {
|
||||
const divided = x.div(BigNumber.from(10).pow(decimals - 6));
|
||||
|
||||
return divided.toNumber() / 1000000;
|
||||
}
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
# URL of the ethereum RPC node to use. Choose this based on your target network
|
||||
# (e.g., this deploys to goerli optimism testnet)
|
||||
RPC_URL=https://goerli.optimism.io
|
||||
RPC_URL=https://endpoints.omniatech.io/v1/matic/mumbai/public
|
||||
|
||||
# The address of the Pyth contract on your network. See the list of contract addresses here https://docs.pyth.network/pythnet-price-feeds/evm
|
||||
PYTH_CONTRACT_ADDRESS="0xff1a0f4744e8582DF1aE09D5611b887B6a12925C"
|
||||
# The Pyth price feed ids of the base and quote tokens. The list of ids is available here https://pyth.network/developers/price-feed-ids
|
||||
# Note that each feed has different ids on mainnet and testnet.
|
||||
BASE_FEED_ID="0x08f781a893bc9340140c5f89c8a96f438bcfae4d1474cc0f688e3a52892c7318"
|
||||
QUOTE_FEED_ID="0x41f3625971ca2ed2263e78573fe5ce23e13d2558ed3f2e47ab0f84fb9e7ae722"
|
||||
QUOTE_FEED_ID="0x1fc18861232290221461220bd4e2acd1dcdfbc89c84092c93c18bdc7756c1588"
|
||||
# The address of the base and quote ERC20 tokens.
|
||||
BASE_ERC20_ADDR="0x8e2a09b54fF35Cc4fe3e7dba68bF4173cC559C69"
|
||||
QUOTE_ERC20_ADDR="0x98cDc14fe999435F3d4C2E65eC8863e0d70493Df"
|
||||
BASE_ERC20_ADDR="0xB3a2EDFEFC35afE110F983E32Eb67E671501de1f"
|
||||
QUOTE_ERC20_ADDR="0x8C65F3b18fB29D756d26c1965d84DBC273487624"
|
||||
|
||||
# Note the -l here uses a ledger wallet to deploy your contract. You may need to change this
|
||||
# option if you are using a different wallet.
|
||||
|
|
|
@ -76,6 +76,8 @@ contract OracleSwap {
|
|||
// We need to round this result in favor of the contract.
|
||||
uint256 quoteSize = (size * basePrice) / quotePrice;
|
||||
|
||||
// TODO: use confidence interval
|
||||
|
||||
if (isBuy) {
|
||||
// (Round up)
|
||||
quoteSize += 1;
|
||||
|
|
Loading…
Reference in New Issue