สร้าง dapp บน Tezos (ฉบับปี 2023)
ส่วนที่ 3 จาก 4-> การแลกเปลี่ยนโทเค็น
ตอนนี้ มาลงหลุมกระต่ายและใช้คุณสมบัติที่ซับซ้อนที่สุดของ dapp: การสลับ XTZ และ tzBTC
การออกแบบ UI
ฉันพูดว่า "ซับซ้อนที่สุด" เพราะอินเทอร์เฟซที่คุณกำลังจะสร้างประกอบด้วยชิ้นส่วนที่เคลื่อนไหวได้จำนวนมากและการคำนวณที่ต้องทำในขณะที่ผู้ใช้ป้อนและยืนยัน สัญญา Liquidity Baking ยังค่อนข้างพิถีพิถันเกี่ยวกับข้อมูลที่คุณต้องส่งเพื่อแลกเปลี่ยนโทเค็น ดังนั้นคุณจะต้องปรับแต่งโค้ดของเราอย่างละเอียดเพื่อให้แน่ใจว่ามันจะทำงานเหมือนเครื่องจักร!
นี่คือภาพหน้าจอของ UI ที่คุณต้องการ:
มีการป้อนข้อความ 2 รายการ ทางด้านซ้ายสามารถแก้ไขได้และจะให้ผู้ใช้ป้อนจำนวน XTZ หรือ tzBTC ที่พวกเขาต้องการแลกเปลี่ยน และทางด้านขวาจะปิดใช้งานและจะแสดงจำนวนเงินที่สอดคล้องกันที่พวกเขาจะได้รับใน โทเค็นอื่น ๆ ปุ่มตรงกลางที่มีลูกศร 2 อันจะช่วยให้ผู้ใช้สามารถสลับอินพุตระหว่าง XTZ และ tzBTC
การลงรายละเอียดเกี่ยวกับวิธีใช้งานการป้อนข้อความจะเกินขอบเขตของบทช่วยสอนนี้ แต่คุณสามารถดูได้ในUserInput.svelteไฟล์
การจัดการอินพุตของผู้ใช้
โดยสรุป แต่ละอินพุตที่มีไอคอนโทเค็นและmaxฟิลด์เป็นองค์ประกอบเดียวกัน คอมโพเนนต์หลักจะติดตามตำแหน่งของแต่ละรายการเพื่ออัปเดต UI ให้สอดคล้องกัน ภายใน ส่วนประกอบอินพุตแต่ละรายการจะติดตามอินพุตของผู้ใช้และยอดคงเหลือที่มีอยู่เพื่อแสดงข้อความแสดงข้อผิดพลาดหากยอดคงเหลือต่ำเกินไป การอัปเดตแต่ละครั้งในอินพุตจะถูกส่งไปยังองค์ประกอบหลักเพื่อปรับ UI ทั่วไป
ทุกครั้งที่ส่งการอัปเดตไปยังองค์ประกอบหลัก ( SwapView.svelte) ข้อมูลที่มีการอัปเดตจะถูกส่งผ่านไปยังsaveInputฟังก์ชัน:
import {
xtzToTokenTokenOutput,
tokenToXtzXtzOutput,
calcSlippageValue
} from "../lbUtils";
const saveInput = ev => {
const { token, val, insufficientBalance: insufBlnc } = ev.detail;
insufficientBalance = insufBlnc;
if (token === tokenFrom && val > 0) {
inputFrom = val.toString();
inputTo = "";
if (tokenFrom === "XTZ") {
// calculates tzBTC amount
let tzbtcAmount = xtzToTokenTokenOutput({
xtzIn: val * 10 ** XTZ.decimals,
xtzPool: $store.dexInfo.xtzPool,
tokenPool: $store.dexInfo.tokenPool
});
if (tzbtcAmount) {
inputTo = tzbtcAmount.dividedBy(10 ** tzBTC.decimals).toPrecision(6);
}
// calculates minimum output
minimumOutput = calcSlippageValue("tzBTC", +inputTo, +slippage);
} else if (tokenFrom === "tzBTC") {
// calculates XTZ amount
let xtzAmount = tokenToXtzXtzOutput({
tokenIn: val * 10 ** tzBTC.decimals,
xtzPool: $store.dexInfo.xtzPool,
tokenPool: $store.dexInfo.tokenPool
});
if (xtzAmount) {
inputTo = xtzAmount.dividedBy(10 ** XTZ.decimals).toPrecision(8);
}
// calculates minimum output
minimumOutput = calcSlippageValue("XTZ", +inputTo, +slippage);
}
} else {
inputFrom = "";
inputTo = "";
}
};
- ค่าที่จำเป็นสำหรับการคำนวณจำนวนโทเค็นจะถูกทำลายจาก
ev.detailวัตถุ - ฟังก์ชันตรวจสอบว่าได้รับค่าจากโทเค็นที่ใช้งานอยู่ในปัจจุบัน (อันทางซ้าย)
- หากโทเค็นนั้นคือ XTZ จำนวนเงินใน tzBTC จะถูกคำนวณผ่านฟังก์ชัน
xtzToTokenTokenOutput(เพิ่มเติมด้านล่าง) - หากโทเค็นนั้นคือ tzBTC จำนวนเงินใน XTZ จะถูกคำนวณผ่านฟังก์ชัน
tokenToXtzXtzOutput(เพิ่มเติมด้านล่าง) - จำนวนเงินขั้นต่ำที่คาดว่าจะเป็นไปตาม Slippage ที่กำหนดโดยผู้ใช้จะคำนวณโดย
calcSlippageฟังก์ชัน
แลกเปลี่ยน XTZ เป็น tzBTC และ tzBTC เป็น XTZ
ตอนนี้ มาดูฟังก์ชันที่เราแนะนำไปข้างต้นxtzToTokenTokenOutputและ tokenToXtzXtzOutputพวกเขาดัดแปลงมาจากรหัสใน repo นี้และให้คุณคำนวณจำนวน tzBTC ที่ผู้ใช้จะได้รับตามจำนวน XTZ ที่พวกเขาป้อนและในทางกลับกัน
export const xtzToTokenTokenOutput = (p: {
xtzIn: BigNumber | number;
xtzPool: BigNumber | number;
tokenPool: BigNumber | number;
}): BigNumber | null => {
let { xtzIn, xtzPool: _xtzPool, tokenPool } = p;
let xtzPool = creditSubsidy(_xtzPool);
let xtzIn_ = new BigNumber(0);
let xtzPool_ = new BigNumber(0);
let tokenPool_ = new BigNumber(0);
try {
xtzIn_ = new BigNumber(xtzIn);
xtzPool_ = new BigNumber(xtzPool);
tokenPool_ = new BigNumber(tokenPool);
} catch (err) {
return null;
}
if (
xtzIn_.isGreaterThan(0) &&
xtzPool_.isGreaterThan(0) &&
tokenPool_.isGreaterThan(0)
) {
const numerator = xtzIn_.times(tokenPool_).times(new BigNumber(998001));
const denominator = xtzPool_
.times(new BigNumber(1000000))
.plus(xtzIn_.times(new BigNumber(998001)));
return numerator.dividedBy(denominator);
} else {
return null;
}
};
เช่นเดียวกับtokenToXtzXtzOutput:
export const tokenToXtzXtzOutput = (p: {
tokenIn: BigNumber | number;
xtzPool: BigNumber | number;
tokenPool: BigNumber | number;
}): BigNumber | null => {
const { tokenIn, xtzPool: _xtzPool, tokenPool } = p;
let xtzPool = creditSubsidy(_xtzPool);
let tokenIn_ = new BigNumber(0);
let xtzPool_ = new BigNumber(0);
let tokenPool_ = new BigNumber(0);
try {
tokenIn_ = new BigNumber(tokenIn);
xtzPool_ = new BigNumber(xtzPool);
tokenPool_ = new BigNumber(tokenPool);
} catch (err) {
return null;
}
if (
tokenIn_.isGreaterThan(0) &&
xtzPool_.isGreaterThan(0) &&
tokenPool_.isGreaterThan(0)
) {
let numerator = new BigNumber(tokenIn)
.times(new BigNumber(xtzPool))
.times(new BigNumber(998001));
let denominator = new BigNumber(tokenPool)
.times(new BigNumber(1000000))
.plus(new BigNumber(tokenIn).times(new BigNumber(999000)));
return numerator.dividedBy(denominator);
} else {
return null;
}
};
การสร้างธุรกรรมการแลกเปลี่ยน
การสลับโทเค็นนั้นค่อนข้างเข้มข้นเนื่องจากเป็นชิ้นส่วนที่เคลื่อนไหวได้หลายอย่างที่ต้องเล่นพร้อมกัน มาอธิบายทีละขั้นตอนว่าจะเกิดอะไรขึ้นหลังจากที่ผู้ใช้คลิกที่ ปุ่ม สลับ :
const swap = async () => {
try {
if (isNaN(+inputFrom) || isNaN(+inputTo)) {
return;
}
...
} catch (error) {
console.log(error);
swapStatus = TxStatus.Error;
store.updateToast(true, "An error has occurred");
}
};
ต่อไป ให้คุณอัปเดต UI เพื่อแสดงให้ผู้ใช้เห็นว่าธุรกรรมกำลังเตรียมพร้อม:
enum TxStatus {
NoTransaction,
Loading,
Success,
Error
}
swapStatus = TxStatus.Loading;
store.updateToast(true, "Waiting to confirm the swap...");
const lbContract = await $store.Tezos.wallet.at(dexAddress);
const deadline = calcDeadline();
หลังจากนั้น คุณสร้างContractAbstractionจาก Taquito เพื่อโต้ตอบกับ DEX และคำนวณกำหนดเวลาด้วย
หมายเหตุ: สัญญา Liquidity Baking คาดว่าคุณจะผ่านกำหนดเวลาสำหรับการแลกเปลี่ยน การทำธุรกรรมจะถูกปฏิเสธหากกำหนดเวลาหมดอายุ
การแลกเปลี่ยน tzBTC สำหรับ XTZ
ตอนนี้ คุณมี 2 สถานการณ์: ผู้ใช้เลือก XTZ หรือ tzBTC เป็นโทเค็นเพื่อแลกเปลี่ยน เริ่มจาก tzBTC เนื่องจากการเตรียมการทำธุรกรรมนั้นซับซ้อนกว่า:
if (tokenFrom === "tzBTC") {
const tzBtcContract = await $store.Tezos.wallet.at(tzbtcAddress);
const tokensSold = Math.floor(+inputFrom * 10 ** tzBTC.decimals);
let batch = $store.Tezos.wallet
.batch()
.withContractCall(tzBtcContract.methods.approve(dexAddress, 0))
.withContractCall(
tzBtcContract.methods.approve(dexAddress, tokensSold)
)
.withContractCall(
lbContract.methods.tokenToXtz(
$store.userAddress,
tokensSold,
minimumOutput,
deadline
)
)
.withContractCall(tzBtcContract.methods.approve(dexAddress, 0));
const batchOp = await batch.send();
await batchOp.confirmation();
}
หมายเหตุ 1: คุณสามารถอ่านเพิ่มเติมเกี่ยวกับพฤติกรรมของสัญญา tzBTC และสัญญา FA1.2 อื่นๆ ได้ที่นี่
หมายเหตุ 2: ในทางเทคนิคแล้ว ไม่จำเป็นต้องตั้งค่าการอนุญาตกลับเป็นศูนย์เมื่อสิ้นสุดการทำธุรกรรม (แต่จำเป็นต้องตั้งค่าเป็นศูนย์ที่จุดเริ่มต้น) เป็นเพียงแนวทางปฏิบัติทั่วไปในการป้องกันการอนุญาตที่รอดำเนินการโดยไม่คาดคิด
ขั้นแรก คุณต้องสร้างContractAbstractionสัญญา tzBTC ขณะที่คุณกำลังโต้ตอบกับมัน เมื่อเสร็จแล้ว คุณจะคำนวณจำนวนโทเค็นที่คุณควรอนุมัติตามการคำนวณก่อนหน้านี้ของเรา
หมายเหตุ: เป็นContractAbstractionอินสแตนซ์ที่มีประโยชน์มากจาก Taquito ซึ่งจะเปิดเผยเครื่องมือและคุณสมบัติต่างๆ เพื่อรับรายละเอียดเกี่ยวกับสัญญาที่กำหนดหรือโต้ตอบกับสัญญานั้น
หลังจากนั้น คุณใช้Batch APIที่จัดทำโดย Taquito Batch API ช่วยให้สามารถจัดกลุ่มการดำเนินการหลายรายการเข้าด้วยกันในธุรกรรมเดียว เพื่อประหยัดน้ำมันและเวลาในการดำเนินการ นี่คือวิธีการทำงาน:
- คุณเรียก
batch()เมธอดที่มีอยู่ในwalletหรือcontractคุณสมบัติของอินสแตนซ์ของTezosToolkit - ซึ่งจะส่งคืนอินสแตนซ์แบบแบตช์ด้วยวิธีการต่างๆ ที่คุณสามารถใช้เพื่อสร้างธุรกรรม ในตัวอย่างของเรา
withContractCall()เป็นวิธีที่จะเพิ่มการเรียกสัญญาใหม่ให้กับชุดการดำเนินการ - ในฐานะที่เป็นพารามิเตอร์สำหรับ
withContractCall()คุณส่งผ่านการเรียกสัญญาราวกับว่าคุณจะเรียกมันเอง โดยใช้ชื่อของจุดเข้าใช้งานบนคุณสมบัติmethodsของContractAbstraction - ในกรณีนี้ คุณแบทช์ 1 การดำเนินการเพื่อตั้งค่าการอนุญาตของ LB DEX ภายในสัญญา tzBTC เป็นศูนย์, 1 การดำเนินการเพื่ออนุมัติจำนวนเงินที่ต้องการโดย swap, 1 การดำเนินการเพื่อยืนยันการแลกเปลี่ยนภายในสัญญา LB DEX และ 1 การดำเนินการเพื่อ ตั้งค่าการอนุญาตของ LB DEX กลับเป็นศูนย์
- ในชุดงานที่ส่งคืน คุณเรียกใช้
.send()เมธอดเพื่อปลอมแปลงธุรกรรม ลงนามและส่งไปยัง Tezos mempool ซึ่งส่งคืนการดำเนินการ - คุณสามารถ
awaitยืนยันธุรกรรมได้โดยการเรียก.confirmation()ใช้การดำเนินการที่ส่งคืนในขั้นตอนด้านบน
- ที่อยู่ของบัญชีที่จะรับ XTZ
- จำนวน tzBTC ที่จะขายสำหรับการแลกเปลี่ยน
- จำนวน XTZ ที่คาดว่าจะได้รับ
- กำหนดเส้นตายหลังจากที่ธุรกรรมหมดอายุ
การแลกเปลี่ยน XTZ เป็น tzBTC
นี่จะเป็นความพยายามที่ง่ายขึ้นมาก! ตรวจสอบรหัสก่อน:
const op = await lbContract.methods
.xtzToToken($store.userAddress, minimumOutput, deadline)
.send({ amount: +inputFrom });
await op.confirmation();
- ที่อยู่ของบัญชีที่จะได้รับโทเค็น tzBTC
- จำนวน tzBTC ที่คาดว่าจะได้รับ
- กำหนดเส้นตาย
จำ.send()วิธีการที่คุณเรียกผลลัพธ์ของการเรียกจุดเริ่มต้นได้หรือไม่ หากคุณไม่ทราบ คุณสามารถส่งพารามิเตอร์ไปยังวิธีนี้ หนึ่งในค่าที่สำคัญที่สุดคือจำนวน XTZ ที่จะส่งไปพร้อมกับธุรกรรม เพียงแค่ส่งวัตถุที่มีamountคุณสมบัติและมูลค่าของจำนวน tez ที่คุณต้องการแนบ แค่นั้น!
จากนั้น เช่นเดียวกับธุรกรรมอื่น ๆ คุณจะได้รับวัตถุการดำเนินการและเรียกใช้.confirmation()เพื่อรอการดำเนินการที่จะรวมอยู่ในบล็อกใหม่
การอัปเดต UI
ไม่ว่าการแลกเปลี่ยนจะสำเร็จหรือไม่ สิ่งสำคัญคือต้องให้ข้อเสนอแนะแก่ผู้ใช้ของคุณ
หากการแลกเปลี่ยนสำเร็จ คุณจะเรียกยอดคงเหลือใหม่ของผู้ใช้และให้ข้อเสนอแนะที่มองเห็นได้:
const res = await fetchBalances($store.Tezos, $store.userAddress);
if (res) {
store.updateUserBalance("XTZ", res.xtzBalance);
store.updateUserBalance("tzBTC", res.tzbtcBalance);
store.updateUserBalance("SIRS", res.sirsBalance);
} else {
store.updateUserBalance("XTZ", null);
store.updateUserBalance("tzBTC", null);
store.updateUserBalance("SIRS", null);
}
// visual feedback
store.updateToast(true, "Swap successful!");
หากการแลกเปลี่ยนไม่สำเร็จ คุณจะถูกเปลี่ยนเส้นทางไปยังcatchสาขาที่ซึ่งคุณจะต้องแสดงความคิดเห็นและอัปเดต UI ด้วย:
swapStatus = TxStatus.Error;
store.updateToast(true, "An error has occurred");
สุดท้าย (ตั้งใจเล่นสำนวน) คุณย้ายไปที่finallyสาขาเพื่อรีเซ็ต UI หลังจาก 3 วินาที:
finally {
setTimeout(() => {
swapStatus = TxStatus.NoTransaction;
store.showToast(false);
}, 3000);
}
ดังที่คุณทราบได้จากโค้ดที่เกี่ยวข้อง การแลกเปลี่ยนโทเค็นเป็นการกระทำที่ค่อนข้างซับซ้อน และมีบางสิ่งที่คุณควรคำนึงถึง ทั้งโค้ดที่คุณเขียนและ UI ที่คุณสร้าง:
- พยายามจัดโครงสร้างรหัสของคุณเป็นขั้นตอนต่างๆ ที่ไม่ปะปนกัน เช่น ขั้นตอนที่ 1: อัปเดต UI ก่อนปลอมแปลงธุรกรรม ขั้นตอนที่ 2: ปลอมแปลงธุรกรรม ขั้นตอนที่ 3: ปล่อยธุรกรรม ขั้นตอนที่ 4: อัปเดต UI เป็นต้น .
- อย่าลืมให้คำติชมแก่ผู้ใช้ของคุณ! การอบการดำเนินการใหม่อาจใช้เวลาถึง 30 วินาทีเมื่อเครือข่ายไม่คับคั่ง และนานกว่านั้นหากมีปริมาณการใช้งานมาก ผู้ใช้จะสงสัยว่าเกิดอะไรขึ้นหากคุณไม่ปล่อยให้รอ โดยทั่วไปแล้ว สปินเนอร์หรือแอนิเมชั่นการโหลดเป็นความคิดที่ดีในการระบุว่าแอปกำลังรอการยืนยันบางอย่าง
- ปิดการใช้งาน UI ในขณะที่การทำธุรกรรมอยู่ใน mempool! คุณไม่ต้องการให้ผู้ใช้คลิกที่ ปุ่ม Swapเป็นครั้งที่สอง (หรือสามหรือสี่!) ในขณะที่ blockchain กำลังประมวลผลธุรกรรมที่พวกเขาสร้างขึ้นแล้ว นอกจากจะทำให้พวกเขาเสียเงินมากขึ้นแล้ว ยังอาจทำให้พวกเขาสับสนและสร้างพฤติกรรมที่ไม่คาดคิดใน UI ของคุณอีกด้วย
- รีเซ็ต UI ในตอนท้าย ไม่มีใครต้องการคลิก ปุ่ม รีเฟรชหลังจากการโต้ตอบกับบล็อกเชน เนื่องจาก UI ดูเหมือนจะติดอยู่ในสถานะก่อนหน้า ตรวจสอบให้แน่ใจว่าอินเทอร์เฟซอยู่ในสถานะเดียวกัน (หรือคล้ายกัน) เช่นเดียวกับเมื่อผู้ใช้เปิดครั้งแรก





































![รายการที่เชื่อมโยงคืออะไร? [ส่วนที่ 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)