เที่ยวทิพย์ Decentralized Exchange ไปกับ DeFi ชื่อดังอย่าง 🦄 Uniswap
กลับมาพบกันอีกครั้งทุกวันหยุดนักขัตฤกษ์ แต่ช่วงนี้ด้วยสถานการณ์โควิดที่ทำให้เราออกจากบ้านกันไม่ได้ วันนี้ผมเลยจะพาทุกคนไปท่องเที่ยวโลก DeFi กับ Project ชื่อดังอย่าง Uniswap ที่เป็น Decentralized Exchange ต้นแบบของ Project อื่นๆ แบบ Exclusive สุดๆ เพราะเราจะ Fork Uniswap มารันกันในบ้านแบบไม่ต้องออกไปไหนด้วย Environment ที่รวดเร็วและแข็งแกร่งที่สุดๆขวัญใจเหล่า Developer นั่นก็คือ Localhost (127.0.0.1) ของเรานี่เอง จะเป็นอย่างไรค่อยๆตามมาเรื่อยๆกันครับ
Decentralized Exchange (DEX) แบบรวบรัดตัดบท
ทุกวันนี้เงินตราต่างๆล้วนแล้วแต่มีสกุลเงินมากมาย อาทิ USD, JPY, THB หากเราจะไปท่องเที่ยวหรือซื้อของจากประเทศต่างๆเราก็จะต้องมีเงินในสกุลที่เค้ารับใช่ไหมครับ ดังนั้นบริการแลกเปลี่ยนสกุลเงินต่างๆก็ถือเกิดขึ้น ซึ่งก็จะมีคนกลางที่คอยให้บริการรับแลกเปลี่ยนเงินในสกุลหนึ่งไปเป็นอีกสกุลหนึ่งหรือที่เราเรียกกันว่า Centralized Exchange (CEX) โดยหากเป็นเงิน Fiat ที่เราใช้กันอยู่ก็จะมีทั้งแบบ Online (บริการอย่าง SCB Planet / Krungthai Travel) และ Offline (บูทรับแลกเงินต่างๆ) ตามที่พวกเราได้เห็นกันไปในท้องตลาด
ในลักษณะเดียวกันกับเงิน Crypto อย่าง BTC, ETH, BNB ต่างๆ พวกเราก็มีความต้องการที่จะสลับสับเปลี่ยนไปยังสกุลเงินที่เราต้องการถือใน Wallet ของเรา ซึ่งก็จะมี CEX ที่เกิดขึ้นมากมายมาให้บริการแลกกับเราไม่ว่าจะเป็น Binance, Coinbase, Bitkub, Zipmex เป็นต้น แต่เนื่องด้วยเงิน Crypto นั้นถูกขับเคลื่อนด้วยเทคโนโลยี Blockchain และความน่าเชื่อถือของ Smart Contract ที่รันอยู่บน Blockchain ทำให้เราไม่ต้องพึ่งบริการในแบบ Centrailzed อีกต่อไป ซึ่ง DeFi Project อย่าง Uniswap ถือกำเนิดขึ้นเพื่อบรรเทาความต้องการในการแลกเปลี่ยนเงินในแบบ Automate market maker ได้เองตามกลไกของตลาด ซึ่งผลกำไรต่างๆจากการแลกเปลี่ยนก็จะถูกปันผลกลับไปให้คนที่มาช่วยๆกันลงขันวางเงินในระบบ (Liquidity Provider) โดยเงินที่เราเอามาลงขันก็ไม่ได้หายไปไหนมันก็จะถูก Reserves อยู่ในสิ่งที่เรียกว่า Liquidity Pool (LP) และออก Contract (LP Token) กลับไปให้คนที่มาลงขันกันเพื่อเป็นการยืนยันความเป็นเจ้าของเงินที่ครบถ้วนอยู่ทุกประการ ทั้งหมดนี้ทำให้ Ecosystem ของ Uniswap มี Cashflow ในการให้บริการกับผู้ที่มาใช้บริการแลกเปลี่ยน (Trader) ต่อไป ซึ่งเป็นการขับเคลื่อนกันแบบ Algorithm ใน Smart Contract ที่โปร่งใสและตรวจสอบได้
เรื่องสนุกๆแบบนี้ผมเอามาย่อซะกุดแบบนี้ หากใครสนใจเพิ่มเติมลองหาข้อมูลเพิ่มกันได้นะครับ งั้นผมขอไปต่อกันเลย
เตรียมที่ทางกันให้พร้อมก่อนไป Fork Uniswap มาเล่นกัน
เนื่องด้วย DeFi Project โดยส่วนมากจะรันอยู่บน Blockchain และถูกเขียนด้วยภาษา Solidity ซึ่งจริงๆแล้วเราสามารถที่จะทำงานอยู่บน Remix Ethereum IDE (https://remix.ethereum.org/) ที่ให้บริการอยู่ซึ่งมีความสะดวกสบายสุดๆ แต่ Concept แบบบ้านๆของเราก็ต้องใช้ IDE ที่เราถนัดกันอย่าง VSCode สิครัช เราเลยจะมาเตรียมเครื่องมือดังต่อไปนี้ครับ
1. VSCode และลง Extension Solidity กันไว้ด้วยนะครับ
2. Chrome และลง Metamask 🦊 เพื่อเป็นกระเป๋าเงินเก็บเงินทดสอบของเรากัน ซึ่งแนะนำให้แยก Account สำหรับการทดสอบออกจาก Account ของ Wallet หลักของเรานะครับ
3. NodeJS เนื่องจาก Framework ที่เรากำลังจะใช้งานใช้ NodeJS Runtime
4. Truffle Suite (https://www.trufflesuite.com/) ขุมพลังนี้เองที่จะทำให้เราสามารถสร้าง Ethereum Virtual Machine (EVM) ภายในเครื่อง local machine ของเรา ซึ่งเราสามารถที่จะวางโครงสร้างของ DeFi project ที่ดีตาม Structure ของ Truffle Project และช่วย Development lifecycle ตั้งแต่การ Coding/Debuging, การเขียน Test, การทำ CI/CD Piepline ของ Smart Contracts ได้อย่างง่ายดาย โดยเจ้าตัว Truffle นั้นทำการลงได้ง่ายดายเพียงแค่ปลายนิ้วโดยการสั่งคำสั่งดังนี้
npm install -g truffle
5. Ethereum Test Network สำหรับทดสอบของเรา ซึ่งสามารถเลือกได้ตามนี้ครับ
- Ganache เป็นเครื่องมือจำลอง Ethereum Node มาไว้บนเครื่องแบบ local local ของเรากันครับ และผมแนะนำให้ลง ganache-cli ไว้เพิ่มเพื่อสามารถ Custom configuration ของ Blockchain ที่เรากำลังจะทำงานได้อย่างสะดวกสบายครับ
brew install --cask ganache
npm install -g ganache-cli
- Public Test Network เช่น Rinkeby, Ropsten, Görli, Kovan ซึ่ง Test Network ต่างๆเหล่านี้ก็มีพร้อมให้เราเลือกใช้งานได้บน Metamask ได้อย่างง่ายดายครับ
ซึ่งทั้ง Ganache และแต่ละ Test Network ก็จะมี Configuration ที่ต่างกัน หากใครเลือกใช้ตัวไหนก็อย่างลืม Config ให้ถูกตาม Spec ของแต่ละ Network กันด้วยนะครับ
ถึงช่วงเวลา Unbox Uniswap
เมื่อเรามีเครื่องไม้เครื่องมือต่างๆครบแล้วหลังจากนี้เราเอา Code ของ Uniswap มาแกะกล่องกันดีกว่าฮ่ะ และเนื่องด้วย Uniswap กำลังจะออก Version 3 ในวันที่ 5 May 2021 บทความนี้ผมจะขอแกะตัว V2 ก่อนละกันนะครับเนื่องจากค่อนข้าง Stable แล้วและเป็นต้นแบบ DeFi ของอีกหลายๆ Project เช่น Sushiswap, Pancakeswap เป็นต้นโดยโครงสร้างของ Uniswap V2 จะประกอบไปด้วยชุดของ Smart Contracts ที่มีส่วนที่สำคัญดังนี้ครับ
- Factory Contract (อยู่ในส่วนของ uniswap-v2-core) ชิ้นส่วนนี้จะมีหน้าที่ Manage Contract ของคู่เงินต่างๆที่ Liquidity Provider นำเงินเข้ามาใน Pool โดยเก็บไว้เป็นข้อมูลของคู่เงินทั้งหมดที่มีในระบบให้เราได้แลกเปลี่ยนกัน
- Router Contract (อยู่ในส่วนของ uniswap-v2-periphery) ส่วนนี้จะมีหน้าที่ควบคุมทั้งในส่วนของการให้ Liquidity Provider เข้ามาฝากเงิน และให้ Trader เข้ามาแลกเงินด้วย โดยมี Factory Contract เสมือนเป็นคลังสมบัติให้เข้าไปหาคู่เงินที่มีการวางไว้อยู่ในระบบ
- Frontend (อยู่ในส่วนของ uniswap-interface) ในส่วนนี้ก็จะไม่มีอะไรมากแค่เป็นหน้าบ้านเพื่อให้ผู้ใช้งานสามารถใช้งานได้ง่ายบน Web ซึ่งการติดต่อจากหน้าบ้านก็จะใช้ Web3 RPC เพื่อคุยกับ Smart Contract ที่ Deploy อยู่บน Blockchain
ชิ้นส่วนต่างๆเหล่านี้จะทำงานไหลเป็น Workflow เดียวกัน หากเราจะทดสอบ Ecosystem ของ Uniswap นี้กันแบบบ้านบ้าน เราเริ่มกันด้วย ..
1. สร้างเงินในระบบด้วยมาตราฐาน ERC20 บน Ethereum Chain
ในการสร้างเหรียญหรือ Token บน Blockchain ตามมาตราฐานของ Ethereum นั้นจะต้องอยู่ในรูปแบบที่ทาง Ethereum กำหนด Spec ไว้ซึ่งเราจะเรียกกันว่า ERC20 ซึ่งการสร้างเหรียญนั้นง่ายมาก ยิ่งตอนนี้เราสามารถนำ Library ที่มีชื่อว่า ‘Openzeppelin’ เข้ามาใช้งาน ทำให้เราสามารถสร้างเหรียญของเราบน Blockchain ได้ภายในไม่กี่วินาที
เริ่มจากการสร้าง Solidity Project ด้วย Truffle ตามนี้
truffle init
หลังจากนี้ก็ทำการสร้าง NPM Project และ Install Openzeppelin เข้าไปกัน
npm init -y
npm install --save openzeppelin-solidity
เมื่อ Install Dependency ต่างๆเรียบร้อย ก็ทำการสร้างเหรียญด้วยภาษา Solidity ตาม Spec ดังนี้
pragma solidity =0.5.16;import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import '@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol';contract JO is ERC20, ERC20Detailed {
constructor(uint256 initialSupply) public
ERC20Detailed("JO Token", "JO", 18) {
_mint(msg.sender, initialSupply);
}
}
โดยที่ในการ Deploy Smart Contracts ใน Truffle เราจะต้องทำการสร้าง ‘migrations/2_deploy_contracts.js’ เพื่อเป็นการบอกขั้นตอนการ Deploy Smart Contract ลงบน Blockchain ให้ Truffle รู้ ซึ่งเราจะใส่รายละเอียดตามนี้ครับ
const web3 = require('web3');
const JO = artifacts.require('token/JO.sol');
const OJ = artifacts.require('token/OJ.sol');module.exports = async function(deployer, _network, addresses) {
await deployer.deploy(JO, web3.utils.toWei('1000', 'ether'));
await deployer.deploy(OJ, web3.utils.toWei('1000', 'ether'));
const jo = await JO.deployed();
const oj = await OJ.deployed();
jo.transfer(addresses[0], web3.utils.toWei('1000', 'ether'))
oj.transfer(addresses[0], web3.utils.toWei('1000', 'ether'))
};
จากตัวอย่างผมกำลังสร้างเหรียญขึ้นมาสองเหรียญ ชื่อว่า JO กับ OJ และทำการยัดเงินเข้ากระเป๋าของผมบน Kovan Test Network จำนวน 1000 Token พร้อมแล้วก็รันกันเลย
truffle migrate --network kovan
ซึ่งผลที่ได้ก็เรียบร้อยดีครับ ผมมีเงินในกระเป๋าติดตัวแล้ว 1000 Token
2. Deploy Uniswap’s Smart Contract เข้าไปอยู่ใน Ethereum Chain
หลังจากที่ผมมีเหรียญเป็นของผมเองแล้ว ต่อไปก็ถึงเวลาที่เราจะ Fork Uniswap มายำเล่นกันแล้วครับ เริ่มจาก
mkdir uniswap && cd uniswap
git clone git@github.com:Uniswap/uniswap-v2-core.git
git clone git@github.com:Uniswap/uniswap-v2-periphery.git
ได้ของมาครบละถึงเวลายำเล่นละฮ่ะ และเนื่องจากมีจุดที่เราต้องแก้ไขให้ใช้งานได้มีอยู่หลายจุดเหมือนกันครับ เรามาตามไปแก้ไปด้วยกันตามนี้ครับ
เริ่มจาก uniwap-v2-core ให้ทำการ init truffle project ขึ้นมา พร้อมกับสร้าง deploy_contract.js แบบเดียวกับที่เราสร้างเหรียญกันครับ แต่เนื่องจาก factory นี้จะเป็นเสมือนคลังสมบัติที่ต้องการความปลอดภัยที่เข้มงวดจึงมีการสร้าง hash ไว้กับ UniswapV2Pair.sol ครับ ผมลองพยายาม Compile เท่าไรก็ไม่ได้ Hash code เดียวกับที่ทาง Uniswap config ไว้ใน SDK งานจึงงอกเลยฮ่ะ เราเลยต้องมาคำนวน Hash กันใหม่เพื่อเอาไปใส่ตามจุดต่างๆ ซึ่งเราจะสร้าง Contract ตามนี้ครับ
pragma solidity =0.5.16;import './UniswapV2Pair.sol';contract CalHash {
function getInitHash() public pure returns(bytes32){
bytes memory bytecode = type(UniswapV2Pair).creationCode;
return keccak256(abi.encodePacked(bytecode));
}
}
โดยที่ deploy_contract ที่ผมจะทำการ deploy จะมี Steps ดังนี้ครับ
const UniswapV2Factory = artifacts.require("UniswapV2Factory.sol");
const CalHash = artifacts.require('CalHash.sol');module.exports = async function(deployer, _network, addresses) {
await deployer.deploy(UniswapV2Factory, addresses[0]);
const factory = await UniswapV2Factory.deployed();
await deployer.deploy(CalHash);
const calhash = await CalHash.deployed();
console.log("Init Code Hash of UniswapV2Pair = " + (await calhash.getInitHash()).toString());
};
จะมีเรื่องของ Compiler Config สำหรับ Truffle นิดนึงที่อาจจะต้องทำการปรับด้วยนะครับ ซึ่งอาจจะปรับตามผมเลยก็ได้ครับ ส่วน Config เรื่องอื่นๆนี่ตามอัธยาศัยกันเลยนะครับ ขึ้นกับ Test Network ที่ใช้กันครับ
compilers: {
solc: {
version: "0.6.6", // Up to Solidity pragma using on .sol
settings: {
optimizer: {
enabled: true,
runs: 999999
},
evmVersion: "istanbul",
outputSelection: {
"*": {
"" : ["ast"],
"*": [
"evm.bytecode.object",
"evm.deployedBytecode.object",
"abi",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.sourceMap",
"metadata"
]
},
}
}
}
หลังจากที่เราทำการ Deploy Factory Contract เข้าไปใน chain แล้ว เราจะได้ของสองอย่างให้เก็บไว้ในใจก่อนนะครับ ได้แก่ ‘Factory Contract Address’ และ ‘Init Code Hash of UniswapV2Pair’
ต่อจาก uniwap-v2-core คือการ deploy router contract ที่อยู่ในส่วนของ uniswap-v2-periphery ครับ ซึ่งเราจะโมดิฟายตามนี้ครับ
ให้เรา truffle init ขึ้นมาใน uniswap-v2-periphery แบบเดียวกันเลยครับ แล้วเราจะแก้ไขในส่วนของ contracts/library/UniswapV2Library.sol ตามค่า Hash UniswapV2Pair ที่เราได้มาข้างต้นครับ
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'<Init Code Hash of UniswapV2Pair>'
))));
}
ก่อนที่เราจะทำการ Deploy Smart Contracts ชุดนี้ ยังมีอีกสิ่งหนึ่งที่ต้องทำก่อนคือ เราจะต้องมี WETH Token กันก่อนครับ ซึ่ง WETH Token นี้คือ Token ที่จะเป็นตัวทำให้ ETH กลายเป็น ERC20 ไปโดยปริยาย โดยมีอัตราการแลกเปลี่ยนเป็น 1:1 กล่าวคือราคาเท่ากันเป็ะ ซึ่งที่ต้องทำแบบนี้เนื่องจากว่า ตามมาตราฐานของ Uniswap V2 นั้นเราจะสามารถแลกเปลี่ยน ECR20 ระหว่างกันได้เองโดยที่ไม่ต้องมี ETH เหมือนใน V1 ครับ ทั้งนี้การที่เราจะมี WETH บน Project ของเราได้ก็คือการไปเอามาจาก Etherscan นี้เลยครับ เราไปเก็บกันมาจาก Address นี้ได้เลยย ..
https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code
หรือสำหรับคนที่ต้องการไประบุ WETH ตรงๆจาก Test Network เลย List ด้านล่างจะเป็น Address ของ WETH ตาม Network ต่างๆครับ
- Mainnet: ’0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
- Ropsten: ’0xc778417E063141139Fce010982780140Aa0cD5Ab’
- Rinkeby: ’0xc778417E063141139Fce010982780140Aa0cD5Ab’
- Goerli: ’0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6'
- Kovan: ’0xd0A1E359811322d97991E03f863a0C30C2cF029C’
หลังจากที่เราได้ WETH.sol มาไว้ใน Workingspace ภายใน Project เราแล้ว และก็ถึงเวลา Deploy Smart Contracts ในส่วนที่เป็น Router กันละครับ ซึ่งเราจะ Define Steps ใน deploy_contract กันตามนี้
const UniswapV2Router02 = artifacts.require("UniswapV2Router02.sol");
const WETH = artifacts.require('token/WETH.sol');
const web3 = require('web3');module.exports = async function (deployer, network, addresses) {
const FACTORY_CONTRACT_ADDRESS = '<Factory Contract Address>'; await deployer.deploy(WETH);
const weth = await WETH.deployed(); await deployer.deploy(UniswapV2Router02, FACTORY_CONTRACT_ADDRESS, weth.address);
};
หลังจากที่เรา Deploy ทั้งสองชุดนี้เรียบร้อยแล้วสรุปเราจะได้สิ่งต่างๆตามนี้นะครับ
- Factory Contract Address
- Router Contract Address
- Hash ของ UniswapV2Pair
3. เตรียมความพร้อมให้ใช้งานง่ายๆด้วย Interface ของ Uniswap
เมื่อมี Smart Contracts ให้ใช้งานแล้ว สถานีต่อไปคือหน้าบ้านละครับ ด้วยตัวของ uniswap-interface ถูกพัฒนาด้วย React Framework เราจะมาเริ่ม clone กันก่อนครับ
cd uniswap
git clone git@github.com:Uniswap/uniswap-v2-sdk.git
git clone git@github.com:Uniswap/uniswap-interface.git
โดยที่ uniswap-v2-sdk นั้นเป็น node module ตัวนึงที่เราจะต้องแก้ไขค่า constants เพื่อให้สามารถทำงานร่วมกันได้กับ Smart Contacts ที่เราได้ Deploy กันไป โดยเราจะไปแก้ในไฟล์ src/constants.ts
export const FACTORY_ADDRESS = '<Factory Contract Address>'
export const INIT_CODE_HASH = '0x<Init code hash of UniswapV2Pair>'
ต่อไปขยับไปในส่วนของ uniswap-v2-interface ครับ เราจะต้องไปแก้ไขดังต่อไปนี้
ในไฟล์ ‘src/constants/index.ts’
export const ROUTER_ADDRESS = '<Router Contract Address>'
ไฟล์ ‘.env’ ซึ่งเราจะต้อง Config ให้ตรงตาม Test Network ที่เราใช้งานอยู่ ซึ่งหากเป็น Public Test Network ผมจะแนะนำเป็นตัว infura.io ซึ่งสามารถใช้งานได้ฟรี 1แสน Requests ต่อวันครับ
REACT_APP_CHAIN_ID="<Chain ID of Blockchain Network>"
REACT_APP_NETWORK_URL="<WEB3 RPC Endpoint>"
ไฟล์ ‘src/swap/hook.ts’ เพื่อ Update address ที่ไม่ควรจะเป็นปลายทางเอาไว้ในระบบ
const BAD_RECIPIENT_ADDRESSES: string[] = [
'<Factory Contract Address>', // v2 factory
'<Router Contract Address>' // v2 router
]
หลังจากนั้นคือการ Update package.json ให้เลือกใช้ uniswap-v2-sdk ที่เราแก้ไขไปแล้วข้างต้นก่อนทำการ start uniswap-interface ขึ้นมาทำงานครับ
yarn
yarn start
4. สร้างคู่ Pair ของเงิน นำเงินไปลงขัน เพื่อออกเป็น LP Token (ERC20) ให้กับ Liquidity Provider
หลังจากนี้ทุกชิ้นส่วนเราน่าจะพร้อมหมดละครับ เรามาทดสอบ Flow กันด้วยการฝากคู่เหรียญเข้า Liquidity Pool เพื่อได้ LP Token กันครับ
หลังจากที่เราทดสอบสร้างคู่เหรียญและยัดเหรียญของเราลงไปใน Liquidity Pool ตัว Smart Contracts ก็ทำการสร้าง LP Token มาให้เราถือในชื่อของ UNI-V2 เป็นที่เรียบร้อยแล้วครับ หลังจากนี้ระบบจะให้เอา LP Token ไป Stake ไรก็ว่ากันไป แต่เราจะตามไปดูต่อกันครับว่าหลังจากที่มี Liquidity ในระบบแล้ว เราจะทำการ Swap ต่อไปว่าจะได้ไหมหว่า
5. แลกเงินในระบบโดยใช้คู่ Pair ของเงินที่ Liquidity Provider นำเงินไปลงไว้
เราจะทำการทดสอบ Swap เหรียญของเราซัก 10 เหรียญดูกันนะครับว่าได้ไหมเอ่ย ..
Bravo! แลกได้เรียบร้อยแล้วครัช ทั้งหมดทั้งมวลนี้เราจะเห็นว่าจากที่เราต้องแลกเงินกับธนาคาร หรือร้านรับแลกเงิน ไม่ว่าจะเป็นการทำธุรกรรมออนไลน์ Centralized Exchange เหล่านั้น ก็จะมีศูนย์กลางที่มีคนคอยควบคุมอยู่ หาผลกำไรจากอัตราแลกเปลี่ยน แต่ Exchange ในแบบ Decentralized Exchange นั้นไม่มีใครควบคุมได้นอกจาก Algorithm ที่ตรวจสอบได้อย่างโปร่งใส โดยที่ทุกอย่างสามารถขับเคลื่อนด้วย Ecosystem ของตัวเองได้ หากมีอัตราการแลกเปลี่ยนที่ไม่เท่ากันระหว่าง Exchange ใด Exchange หนึ่ง ก็จะมีกลไล Arbitrage ที่จะมีคนเขียน Bot ขึ้นมากินส่วนต่างเหล่านี้เอง โดยที่ทั้งหมดควบคุมโดยทุกคนเพื่อทุกคนอย่างเท่าเทียมจริงๆครับ
เรื่องราวทั้งหมดก็เป็นช่วงวันหยุดว่างๆในวันสงกรานต์ของผมที่ไปไหนก็ไม่ได้ แต่ก็ถือว่ามีโอกาสได้ทำไรสนุกๆ เข้าใจเบื้องลึกเบื้องหลังของ DeFi ชั้นนำอย่าง Uniswap มากกว่าแค่การเป็น Crypto Trader คนนึง สุดท้ายนี้ต้องขอบคุณทุกๆความรู้บน Internet ที่ผมได้อ่านและได้นำมาใช้แก้ปัญหาซึ่งอาจจะเยอะมากจนอาจจะ Reference ได้ไม่หมด 🙏🏻
ขอบคุณครับที่ตามอ่านกันจนจบนะค้าบบ 🦄