สร้าง dapp บน Tezos (ฉบับปี 2023)

Feb 25 2023
ส่วนที่ 3 ของ 4-> การแลกเปลี่ยนโทเค็น ตอนนี้ เรามาเจาะลึกและใช้คุณสมบัติที่ซับซ้อนที่สุดของ dapp: การแลกเปลี่ยน XTZ และ tzBTC การออกแบบ UI ที่ฉันพูดว่า "ซับซ้อนที่สุด" เพราะอินเทอร์เฟซที่คุณกำลังจะสร้างประกอบด้วยชิ้นส่วนที่เคลื่อนไหวได้จำนวนมากและการคำนวณที่ต้องทำในขณะที่ผู้ใช้ป้อนและยืนยัน

ส่วนที่ 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 ช่วยให้สามารถจัดกลุ่มการดำเนินการหลายรายการเข้าด้วยกันในธุรกรรมเดียว เพื่อประหยัดน้ำมันและเวลาในการดำเนินการ นี่คือวิธีการทำงาน:

  1. คุณเรียกbatch()เมธอดที่มีอยู่ในwalletหรือcontractคุณสมบัติของอินสแตนซ์ของTezosToolkit
  2. ซึ่งจะส่งคืนอินสแตนซ์แบบแบตช์ด้วยวิธีการต่างๆ ที่คุณสามารถใช้เพื่อสร้างธุรกรรม ในตัวอย่างของเราwithContractCall()เป็นวิธีที่จะเพิ่มการเรียกสัญญาใหม่ให้กับชุดการดำเนินการ
  3. ในฐานะที่เป็นพารามิเตอร์สำหรับwithContractCall()คุณส่งผ่านการเรียกสัญญาราวกับว่าคุณจะเรียกมันเอง โดยใช้ชื่อของจุดเข้าใช้งานบนคุณสมบัติmethodsของContractAbstraction
  4. ในกรณีนี้ คุณแบทช์ 1 การดำเนินการเพื่อตั้งค่าการอนุญาตของ LB DEX ภายในสัญญา tzBTC เป็นศูนย์, 1 การดำเนินการเพื่ออนุมัติจำนวนเงินที่ต้องการโดย swap, 1 การดำเนินการเพื่อยืนยันการแลกเปลี่ยนภายในสัญญา LB DEX และ 1 การดำเนินการเพื่อ ตั้งค่าการอนุญาตของ LB DEX กลับเป็นศูนย์
  5. ในชุดงานที่ส่งคืน คุณเรียกใช้.send()เมธอดเพื่อปลอมแปลงธุรกรรม ลงนามและส่งไปยัง Tezos mempool ซึ่งส่งคืนการดำเนินการ
  6. คุณสามารถ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 ดูเหมือนจะติดอยู่ในสถานะก่อนหน้า ตรวจสอบให้แน่ใจว่าอินเทอร์เฟซอยู่ในสถานะเดียวกัน (หรือคล้ายกัน) เช่นเดียวกับเมื่อผู้ใช้เปิดครั้งแรก