diff --git a/swap-demo-tutorial-part-tyler/.gitkeep b/swap-demo-tutorial-part-tyler/.gitkeep
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/swap-demo-tutorial-part-tyler/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/swap-demo-tutorial-part-tyler/index.html b/swap-demo-tutorial-part-tyler/index.html
new file mode 100644
index 00000000..4b865aa6
--- /dev/null
+++ b/swap-demo-tutorial-part-tyler/index.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+ Swap Demo Tutorial
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/swap-demo-tutorial-part-tyler/index.js b/swap-demo-tutorial-part-tyler/index.js
new file mode 100644
index 00000000..d9f382db
--- /dev/null
+++ b/swap-demo-tutorial-part-tyler/index.js
@@ -0,0 +1,193 @@
+let currentTrade = {};
+let currentSelectSide;
+let tokens;
+
+async function init() {
+ await listAvailableTokens();
+}
+
+async function listAvailableTokens(){
+ console.log("initializing");
+ let response = await fetch('https://tokens.coingecko.com/uniswap/all.json');
+ let tokenListJSON = await response.json();
+ console.log("listing available tokens: ", tokenListJSON);
+ tokens = tokenListJSON.tokens;
+ console.log("tokens: ", tokens);
+
+ // Create token list for modal
+ let parent = document.getElementById("token_list");
+ for (const i in tokens){
+ // Token row in the modal token list
+ let div = document.createElement("div");
+ div.className = "token_row";
+ let html = `
+ ${tokens[i].symbol}
+ `;
+ div.innerHTML = html;
+ div.onclick = () => {
+ selectToken(tokens[i]);
+ };
+ parent.appendChild(div);
+ };
+}
+
+async function selectToken(token){
+ closeModal();
+ currentTrade[currentSelectSide] = token;
+ console.log("currentTrade: ", currentTrade);
+ renderInterface();
+}
+
+function renderInterface(){
+ if (currentTrade.from){
+ console.log(currentTrade.from)
+ document.getElementById("from_token_img").src = currentTrade.from.logoURI;
+ document.getElementById("from_token_text").innerHTML = currentTrade.from.symbol;
+ }
+ if (currentTrade.to){
+ console.log(currentTrade.to)
+ document.getElementById("to_token_img").src = currentTrade.to.logoURI;
+ document.getElementById("to_token_text").innerHTML = currentTrade.to.symbol;
+ }
+}
+
+async function connect() {
+ if (typeof window.ethereum !== "undefined") {
+ try {
+ console.log("connecting");
+ await ethereum.request({ method: "eth_requestAccounts" });
+ } catch (error) {
+ console.log(error);
+ }
+ document.getElementById("login_button").innerHTML = "Connected";
+ // const accounts = await ethereum.request({ method: "eth_accounts" });
+ document.getElementById("swap_button").disabled = false;
+ } else {
+ document.getElementById("login_button").innerHTML = "Please install MetaMask";
+ }
+}
+
+function openModal(side){
+ currentSelectSide = side;
+ document.getElementById("token_modal").style.display = "block";
+}
+
+function closeModal(){
+ document.getElementById("token_modal").style.display = "none";
+}
+
+// 替代 qs 功能
+function build_query_string(obj) {
+ let allKeys = Object.keys(obj);
+ let allVals = Object.values(obj);
+ let arr = [];
+ allKeys.forEach((k, i) => {
+ arr.push(`${k}=${allVals[i]}`)
+ });
+ let str = arr.join("&");
+ return str;
+}
+
+async function getPrice(){
+ console.log("Getting Price");
+
+ if (!currentTrade.from || !currentTrade.to || !document.getElementById("from_amount").value) return;
+ let amount = Number(document.getElementById("from_amount").value * 10 ** currentTrade.from.decimals);
+
+ const params = {
+ sellToken: currentTrade.from.address,
+ buyToken: currentTrade.to.address,
+ sellAmount: amount,
+ }
+
+ const str = build_query_string(params);
+ const headers = { '0x-api-key': '9dab2060-ab34-4e6a-9c20-cfb3263692df' }; // This is a placeholder. Get your live API key from the 0x Dashboard (https://dashboard.0x.org/apps)
+
+ // Fetch the swap price.
+ const response = await fetch(`https://api.0x.org/swap/v1/price?${str}`, { headers });
+
+ swapPriceJSON = await response.json();
+ console.log("Price: ", swapPriceJSON);
+
+ document.getElementById("to_amount").value = swapPriceJSON.buyAmount / (10 ** currentTrade.to.decimals);
+ document.getElementById("gas_estimate").innerHTML = swapPriceJSON.estimatedGas;
+}
+
+async function getQuote(account){
+ console.log("Getting Quote");
+
+ if (!currentTrade.from || !currentTrade.to || !document.getElementById("from_amount").value) return;
+ let amount = Number(document.getElementById("from_amount").value * 10 ** currentTrade.from.decimals);
+
+ const params = {
+ sellToken: currentTrade.from.address,
+ buyToken: currentTrade.to.address,
+ sellAmount: amount,
+ takerAddress: account,
+ }
+
+ const str = build_query_string(params);
+ const headers = { '0x-api-key': '9dab2060-ab34-4e6a-9c20-cfb3263692df' }; // This is a placeholder. Get your live API key from the 0x Dashboard (https://dashboard.0x.org/apps)
+
+ // Fetch the swap quote.
+ const response = await fetch(`https://api.0x.org/swap/v1/quote?${str}`, { headers });
+
+ swapQuoteJSON = await response.json();
+ console.log("Quote: ", swapQuoteJSON);
+
+ document.getElementById("to_amount").value = swapQuoteJSON.buyAmount / (10 ** currentTrade.to.decimals);
+ document.getElementById("gas_estimate").innerHTML = swapQuoteJSON.estimatedGas;
+
+ return swapQuoteJSON;
+}
+
+async function trySwap(){
+ const erc20abi= [{ "inputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "symbol", "type": "string" }, { "internalType": "uint256", "name": "max_supply", "type": "uint256" } ], "stateMutability": "nonpayable", "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": "account", "type": "address" } ], "name": "balanceOf", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "burn", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "burnFrom", "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": [], "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": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "transfer", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "transferFrom", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }]
+ console.log("trying swap");
+
+ // Only work if MetaMask is connect
+ // Connecting to Ethereum: Metamask
+ const web3 = new Web3(Web3.givenProvider);
+
+ // The address, if any, of the most recently used account that the caller is permitted to access
+ let accounts = await ethereum.request({ method: "eth_accounts" });
+ let takerAddress = accounts[0];
+ console.log("takerAddress: ", takerAddress);
+
+ const swapQuoteJSON = await getQuote(takerAddress);
+
+ // Set Token Allowance
+ // Set up approval amount
+ const fromTokenAddress = currentTrade.from.address;
+ const maxApproval = new BigNumber(2).pow(256).minus(1);
+ console.log("approval amount: ", maxApproval);
+ const ERC20TokenContract = new web3.eth.Contract(erc20abi, fromTokenAddress);
+ console.log("setup ERC20TokenContract: ", ERC20TokenContract);
+
+ // Grant the allowance target an allowance to spend our tokens.
+ const tx = await ERC20TokenContract.methods.approve(
+ swapQuoteJSON.allowanceTarget,
+ maxApproval,
+ )
+ .send({ from: takerAddress })
+ .then(tx => {
+ console.log("tx: ", tx)
+ });
+
+ // Perform the swap
+ const receipt = await web3.eth.sendTransaction(swapQuoteJSON);
+ console.log("receipt: ", receipt);
+}
+
+init();
+
+document.getElementById("login_button").onclick = connect;
+document.getElementById("from_token_select").onclick = () => {
+ openModal("from");
+};
+document.getElementById("to_token_select").onclick = () => {
+ openModal("to");
+};
+document.getElementById("modal_close").onclick = closeModal;
+document.getElementById("from_amount").onblur = getPrice;
+document.getElementById("swap_button").onclick = trySwap;
diff --git a/swap-demo-tutorial-part-tyler/style.css b/swap-demo-tutorial-part-tyler/style.css
new file mode 100644
index 00000000..4de32863
--- /dev/null
+++ b/swap-demo-tutorial-part-tyler/style.css
@@ -0,0 +1,56 @@
+html, nav{
+ background: #333;
+}
+body{
+ margin: auto;
+ background: #333;
+}
+
+.container{
+ background: #333;
+}
+
+a, a.navbar-brand {color: antiquewhite;}
+
+#window{
+ margin-top: 50px;
+ background-color: #000;
+ color: #fff;
+ padding: 15px;
+ border-radius: 20px;
+ box-shadow: 0 0 5px black;
+}
+.swapbox_select {
+ width: 50%;
+ float: left;
+}
+.swapbox{
+ overflow: auto;
+ margin: 20px 0;
+ padding: 20px;
+ background-color: #2f2f2f;
+ border-radius: 20px;
+ border: 1px solid #565656;
+}
+.token_select{
+ padding:5px 0;
+}
+.token_select:hover{
+ background-color: #464646;
+ cursor: pointer;
+
+}
+.token_row{
+ padding: 5px 10px;
+}
+.token_row:hover{
+ background-color: #e4e4e4;
+ cursor: pointer;
+}
+.gas_estimate_label{
+ padding:5px;
+}
+.modal-body{
+ height: 500px;
+ overflow: scroll;
+}