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

Feb 25 2023
ส่วนที่ 4 จาก 4-> การเพิ่มและลบสภาพคล่อง สิ่งนี้จะเป็นเรื่องใหญ่ แต่มีส่วนร่วมน้อยกว่าการแลกเปลี่ยนโทเค็นเล็กน้อย! ส่วนที่ซับซ้อนที่สุดเกี่ยวกับการเพิ่มสภาพคล่องในสัญญา Liquidity Baking คือการได้รับจำนวนโทเค็นที่ถูกต้อง! หลังจากนั้นจะเป็นการเดินเล่นในสวนสาธารณะ อันดับแรก มาทำความเข้าใจว่าเรากำลังทำอะไรที่นี่: LB DEX ช่วยให้คุณสามารถจัดหาโทเค็นคู่หนึ่ง (มีเพียง 2 ตัวเลือกในที่นี้ XTZ และ tzBTC) เป็นสภาพคล่องเพื่อเปิดใช้งานคุณลักษณะการแลกเปลี่ยน

ส่วนที่ 4 จาก 4-> การเพิ่มและลบสภาพคล่อง

สิ่งนี้จะเป็นเรื่องใหญ่ แต่มีส่วนร่วมน้อยกว่าการแลกเปลี่ยนโทเค็นเล็กน้อย!

ส่วนที่ซับซ้อนที่สุดเกี่ยวกับการเพิ่มสภาพคล่องในสัญญา Liquidity Baking คือการได้รับจำนวนโทเค็นที่ถูกต้อง! หลังจากนั้นจะเป็นการเดินเล่นในสวนสาธารณะ

อันดับแรก มาทำความเข้าใจว่าเรากำลังทำอะไรที่นี่: LB DEX ช่วยให้คุณสามารถจัดหาโทเค็นคู่หนึ่ง (มีเพียง 2 ตัวเลือกในที่นี้ XTZ และ tzBTC) เป็นสภาพคล่องเพื่อเปิดใช้งานคุณลักษณะการแลกเปลี่ยน ในการแลกเปลี่ยน คุณจะได้รับโทเค็น SIRS เพื่อเป็นตัวแทนการลงทุนของคุณ โทเค็นเหล่านี้มีมูลค่าเพิ่มขึ้นเมื่อเวลาผ่านไป ดังนั้นหากคุณรอนานพอ คุณสามารถทำกำไรได้เมื่อคุณลบสภาพคล่องออก ซึ่งจะอธิบายด้านล่าง

อินเทอร์เฟซที่นี่จะดูคล้ายกับอินเทอร์เฟซสำหรับการแลกเปลี่ยน โดยมีข้อแตกต่างที่สำคัญบางประการ:

เหมือนก่อนหน้านี้ เรามีช่องอินพุต 2 ช่อง แต่คราวนี้ไม่มีปุ่มตรงกลางสำหรับสลับระหว่าง 2 โทเค็น และอินพุตทั้งสองสามารถแก้ไขได้

เมื่อป้อนตัวเลขในช่องใดช่องหนึ่ง dapp จะต้องคำนวณจำนวนที่สอดคล้องกันของโทเค็นอื่นๆ รวมถึงจำนวนที่คาดว่าจะได้รับใน SIRS

ทีนี้มาดูกันว่าทั้งหมดนั้นทำอย่างไร!

การแปลงอินพุต

เมื่อผู้ใช้กำลังจะป้อนตัวเลขในฟิลด์ใดฟิลด์หนึ่ง การป้อนข้อมูลจะส่งเหตุการณ์ใหม่ไปยังส่วนประกอบอินเทอร์เฟซพร้อมกับชื่อของโทเค็นที่เกี่ยวข้องและจำนวนเงินที่ป้อน ข้อมูลนี้จะถูกอ่านโดยsaveInputฟังก์ชัน:

const saveInput = ev => {
    const { token, val }: { token: token; val: number | null } = ev.detail;
    ...
}

if (token === "XTZ" && val && val > 0) {
  inputXtz = val.toString();
  let tzbtcAmount = addLiquidityTokenIn({
    xtzIn: val * 10 ** 6,
    xtzPool: $store.dexInfo.xtzPool,
    tokenPool: $store.dexInfo.tokenPool
  });
  if (tzbtcAmount) {
    inputTzbtc = tzbtcAmount.dividedBy(10 ** 8).toPrecision(6);
  } else {
    inputTzbtc = "";
  }
  ...
}

ค่าจะถูกส่งไปยังสตริงและเก็บไว้ในinputXtzตัวแปรเพื่อใช้ในภายหลัง จำนวน tzBTC ที่สอดคล้องกันนั้นคำนวณด้วยaddLiquidityTokenInฟังก์ชัน ซึ่งเป็นอีกหนึ่งฟังก์ชันที่มีประโยชน์ในการคำนวณจำนวนโทเค็นต่างๆ สำหรับ LB DEX นี่คือข้อมูลอ้างอิง:

const addLiquidityTokenIn = (p: {
  xtzIn: BigNumber | number;
  xtzPool: BigNumber | number;
  tokenPool: BigNumber | number;
}): BigNumber | null => {
  const { xtzIn, xtzPool, tokenPool } = p;
  let xtzIn_ = new BigNumber(0);
  let xtzPool_ = new BigNumber(0);
  let tokenPool_ = new BigNumber(0);
  try {
    xtzIn_ = new BigNumber(xtzIn);
    xtzPool_ = creditSubsidy(xtzPool);
    tokenPool_ = new BigNumber(tokenPool);
  } catch (err) {
    return null;
  }
  if (
    xtzIn_.isGreaterThan(0) &&
    xtzPool_.isGreaterThan(0) &&
    tokenPool_.isGreaterThan(0)
  ) {
    return ceilingDiv(xtzIn_.times(tokenPool_), xtzPool_);
  } else {
    return null;
  }
};

หากผู้ใช้ป้อนจำนวนเงินใน tzBTC ขั้นตอนจะคล้ายกันมากในการคำนวณจำนวนเงินที่สอดคล้องกันใน XTZ:

else if (token === "tzBTC" && val && val > 0) {
  inputTzbtc = val.toString();
  let xtzAmount = tokenToXtzXtzOutput({
    tokenIn: val * 10 ** 8,
    xtzPool: $store.dexInfo.xtzPool,
    tokenPool: $store.dexInfo.tokenPool
  });
  if (xtzAmount) {
    inputXtz = xtzAmount.dividedBy(10 ** 6).toPrecision(8);
    ...
  } else {
    inputXtz = "";
  }
}

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;
  }
};

การคำนวณจำนวน SIRS ที่คาดหวัง

ตอนนี้ เราต้องคำนวณจำนวน SIRS ที่สอดคล้องกันซึ่งจะถูกสร้างขึ้นหากinputXtzและinputTzbtcถูกระบุเป็นพารามิเตอร์เพื่อเพิ่มสภาพคล่อง ฟังaddLiquidityLiquidityCreatedก์ชั่นทำงานหนักทั้งหมดสำหรับเรา:

const addLiquidityLiquidityCreated = (p: {
  xtzIn: BigNumber | number;
  xtzPool: BigNumber | number;
  totalLiquidity: BigNumber | number;
}): BigNumber | null => {
  const { xtzIn, xtzPool, totalLiquidity } = p;
  let xtzIn_ = new BigNumber(0);
  let xtzPool_ = new BigNumber(0);
  let totalLiquidity_ = new BigNumber(0);
  try {
    xtzIn_ = new BigNumber(xtzIn);
    xtzPool_ = new BigNumber(xtzPool);
    totalLiquidity_ = new BigNumber(totalLiquidity);
  } catch (err) {
    return null;
  }
  xtzPool_ = creditSubsidy(xtzPool_);

if (xtzIn_.isGreaterThan(0) && xtzPool_.isGreaterThan(0)) {
  if (totalLiquidity_.isEqualTo(0)) {
    return new BigNumber(xtzIn)
    .times(new BigNumber(totalLiquidity))
    .dividedBy(new BigNumber(xtzPool));
  } else if (totalLiquidity_.isGreaterThan(0)) {
    return new BigNumber(xtzIn)
    .times(new BigNumber(totalLiquidity))
    .dividedBy(new BigNumber(xtzPool));
  }
    return null;
  } else {
    return null;
  }
};

  1. จำนวน XTZ ที่คุณต้องการเพิ่มเป็นสภาพคล่อง
  2. สถานะปัจจุบันของพูล XTZ
  3. จำนวนสภาพคล่องทั้งหมดที่มีอยู่ในสัญญา (เช่น โทเค็น SIRS)

การส่งโทเค็น

หลังจากที่เราคำนวณมูลค่าทั้งหมดที่เราต้องการเพื่อเพิ่มสภาพคล่องให้กับสัญญา Liquidity Baking แล้ว ก็ถึงเวลาที่จะปลอมแปลงธุรกรรม!

const addLiquidity = async () => {
  try {
    if (inputXtz && inputTzbtc && sirsOutput) {
      addLiquidityStatus = TxStatus.Loading;
      store.updateToast(
        true,
        "Adding liquidity, waiting for confirmation..."
      );
      const tzbtcForLiquidity = Math.floor(
        +inputTzbtc * 10 ** tzBTC.decimals
      );
      const lbContract = await $store.Tezos.wallet.at(dexAddress);
      const tzBtcContract = await $store.Tezos.wallet.at(tzbtcAddress);
    ...
}

หลังจากนั้น เราจะแปลงจำนวน tzBTC ที่เราได้รับเป็นมูลค่า "จริง" นั่นคือมูลค่าที่ไม่มีจุดทศนิยมตามที่จัดเก็บไว้ในสัญญา

จากนั้น เราสร้างContractAbstractionสำหรับ LB DEX และContractAbstractionสำหรับสัญญา tzBTC เนื่องจากเราจะโต้ตอบกับทั้งสองอย่าง

หมายเหตุ: โปรดจำไว้ว่า ทุกครั้งที่ผู้ใช้ของคุณต้องการใช้ tzBTC กับ LB DEX จำนวนโทเค็นที่จะใช้จะต้องได้รับการอนุมัติในระดับสัญญา tzBTC ซึ่งต้องมีการดำเนินการ 3 แบบที่แตกต่างกัน

ณ จุดนี้ คุณอาจเดาได้ว่าเราต้องสร้างธุรกรรมเป็นชุด แต่ลองทำด้วยวิธีที่แตกต่างจากบทที่แล้ว เพื่อที่คุณจะได้เลือกวิธีที่คุณต้องการ:

const batch = $store.Tezos.wallet.batch([
  {
    kind: OpKind.TRANSACTION,
    ...tzBtcContract.methods.approve(dexAddress, 0).toTransferParams()
  },
  {
    kind: OpKind.TRANSACTION,
    ...tzBtcContract.methods
    .approve(dexAddress, tzbtcForLiquidity)
    .toTransferParams()
  },
  {
    kind: OpKind.TRANSACTION,
    ...lbContract.methodsObject
    .addLiquidity({
      owner: $store.userAddress,
      minLqtMinted: sirsOutput,
      maxTokensDeposited: tzbtcForLiquidity,
      deadline: calcDeadline()
    })
    .toTransferParams(),
    amount: +inputXtz
  },
  {
    kind: OpKind.TRANSACTION,
    ...tzBtcContract.methods.approve(dexAddress, 0).toTransferParams()
  }
]);

const batchOp = await batch.send();
await batchOp.confirmation();

การดำเนินการครั้งแรก:

{
  kind: OpKind.TRANSACTION,
  ...tzBtcContract.methods.approve(dexAddress, 0).toTransferParams()
}

การดำเนินการที่สอง:

{
  kind: OpKind.TRANSACTION,
  ...tzBtcContract.methods
    .approve(dexAddress, tzbtcForLiquidity)
    .toTransferParams()
}

การดำเนินการที่สาม:

{
  kind: OpKind.TRANSACTION,
  ...lbContract
    .methodsObject
    .addLiquidity({
      owner: $store.userAddress,
      minLqtMinted: sirsOutput,
      maxTokensDeposited: tzbtcForLiquidity,
      deadline: calcDeadline()
    })
    .toTransferParams(),
  amount: +inputXtz
}

  1. ที่อยู่ของบัญชีที่จะรับโทเค็น SIRS
  2. จำนวนโทเค็น SIRS ขั้นต่ำที่คาดว่าจะได้รับ
  3. จำนวน tzBTC ที่ฝาก
  4. กำหนดเส้นตาย

การดำเนินการที่สี่:

{
  kind: OpKind.TRANSACTION,
  ...tzBtcContract.methods.approve(dexAddress, 0).toTransferParams()
}

จากนั้น เช่นเดียวกับธุรกรรมอื่นๆ ปลอมแปลงผ่าน Taquito คุณเรียก.send()และ.confirmation()ไปที่วัตถุการดำเนินการเพื่อรอการยืนยันหนึ่งครั้ง

เมื่อธุรกรรมได้รับการยืนยัน คุณจะล้าง UI ก่อนที่จะเรียกยอดคงเหลือใหม่ของ XTZ, tzBTC และ SIRS

หากการทำธุรกรรมล้มเหลว คุณจะอัปเดต UI และให้ข้อเสนอแนะแก่ผู้ใช้:

addLiquidityStatus = TxStatus.Error;
store.updateToast(true, "An error has occurred");

setTimeout(() => {
  addLiquidityStatus = TxStatus.NoTransaction;
  store.showToast(false);
}, 3000);

ขจัดสภาพคล่อง

การนำสภาพคล่องออกจากสัญญา Liquidity Baking นั้นเป็นงานที่ง่ายที่สุดในบรรดางานทั้งหมดที่ทำได้โดยอินเทอร์เฟซของเรา อินเทอร์เฟซต้องการเพียงหนึ่งอินพุตเพื่อรับจำนวน SIRS ที่ผู้ใช้ต้องการแกะเพื่อรับ XTZ และ tzBTC

จากนั้น Dapp จะคำนวณจำนวน XTZ และ tzBTC ที่สอดคล้องกันที่คาดว่าจะได้รับสำหรับจำนวน SIRS ในช่องป้อนข้อมูล

ในlbUtils.tsไฟล์ คุณจะพบremoveLiquidityXtzTzbtcOutฟังก์ชันสำหรับคำนวณจำนวนเหล่านี้:

const outputRes = removeLiquidityXtzTzbtcOut({
  liquidityBurned: val,
  totalLiquidity: $store.dexInfo.lqtTotal.toNumber(),
  xtzPool: $store.dexInfo.xtzPool.toNumber(),
  tokenPool: $store.dexInfo.tokenPool.toNumber()
});
if (outputRes) {
  const { xtzOut, tzbtcOut } = outputRes;
  xtzOutput = xtzOut
    .decimalPlaces(0, 1)
    .dividedBy(10 ** 6)
    .decimalPlaces(6)
    .toNumber();
  tzbtcOutput = tzbtcOut
    .decimalPlaces(0, 1)
    .dividedBy(10 ** 8)
    .decimalPlaces(8)
    .toNumber();
}

  • liquidityBurned-> จำนวน SIRS ที่จะเผาผลาญ
  • totalLiquidity-> จำนวนโทเค็น SIRS ทั้งหมดในสัญญา
  • xtzPool-> จำนวนโทเค็น XTZ ทั้งหมดในสัญญา
  • tokenPool-> จำนวนโทเค็น tzBTC ทั้งหมดในสัญญา

ตอนนี้เรามาดูวิธีการโต้ตอบกับremoveLiquidityจุดเริ่มต้นของสัญญา ขั้นแรก เราสร้างremoveLiquidityฟังก์ชันภายในโค้ด TypeScript ซึ่งจะถูกเรียกใช้เมื่อผู้ใช้คลิกที่Remove liquidityปุ่ม:

const removeLiquidity = async () => {
  try {
  if (inputSirs) {
    removeLiquidityStatus = TxStatus.Loading;
    store.updateToast(
      true,
      "Removing liquidity, waiting for confirmation..."
    );
    const lbContract = await $store.Tezos.wallet.at(dexAddress);
          
  ...
};

ถัดไป a ContractAbstractionถูกสร้างขึ้นสำหรับ LB DEX เพื่อโต้ตอบกับ Taquito

ตอนนี้ เราสามารถปลอมธุรกรรมจริงได้:

const op = await lbContract.methodsObject
  .removeLiquidity({
    to: $store.userAddress,
    lqtBurned: inputSirs,
    minXtzWithdrawn: Math.floor(xtzOutput * 10 ** XTZ.decimals),
    minTokensWithdrawn: Math.floor(tzbtcOutput * 10 ** tzBTC.decimals),
    deadline: calcDeadline()
  })
  .send();
await op.confirmation();

  1. to-> บัญชีที่จะได้รับ XTZ และ tzBTC
  2. lqtBurned-> จำนวน SIRS ที่จะเผาผลาญ
  3. minXtzWithdrawn-> จำนวนขั้นต่ำของ XTZ ที่คาดว่าจะได้รับ
  4. minTokensWithdrawn-> จำนวนขั้นต่ำของ tzBTC ที่คาดว่าจะได้รับ
  5. deadline-> เช่นเดียวกับจุดเริ่มต้นอื่น ๆ จะต้องระบุกำหนดเวลาสำหรับการทำธุรกรรม

หากการทำธุรกรรมสำเร็จ เราจะอัปเดต UI และรีเซ็ตค่าโทเค็นเพื่อแจ้งให้ผู้ใช้ทราบ:

removeLiquidityStatus = TxStatus.Success;
inputSirs = "";
xtzOutput = 0;
tzbtcOutput = 0;
// fetches user's XTZ, tzBTC and SIRS balances
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);
}
store.updateToast(true, "Liquidity successfully removed!");

removeLiquidityStatus = TxStatus.Error;
store.updateToast(true, "An error has occurred");

บทสรุป

คุณทำจนจบบทช่วยสอนนี้แล้ว

Dapp ที่เรียบง่ายนี้แนะนำแนวคิดต่างๆ มากมายที่เป็นพื้นฐานในการพัฒนาแอปพลิเคชันบน Tezos แต่ยังรวมถึงการทำความเข้าใจว่า Tezos ทำงานอย่างไรโดยทั่วไป

Taquito เป็นไลบรารีที่น่าทึ่งสำหรับการพัฒนาบน Tezos ไม่ว่าคุณจะต้องการสร้างต้นแบบแนวคิดอย่างรวดเร็วหรือต้องการสร้างแอปพลิเคชันแบบกระจายศูนย์แบบฟูลสแตก มีไลบรารีหลักพร้อมทุกสิ่งที่คุณต้องการในการอ่านจาก Tezos blockchain โต้ตอบกับสัญญาอัจฉริยะและใช้กระเป๋าเงิน และแพ็คเกจขนาดเล็กจำนวนมากสำหรับการใช้งานเฉพาะ เช่น การอ่านข้อมูลเมตาของโทเค็นหรือการดำเนินการแบทช์

ไม่ว่าคุณจะต้องการสร้างแอปส่วนหน้า แอปส่วนหลัง หรือแม้แต่แอปเดสก์ท็อป ตราบใดที่คุณใช้ JavaScript/NodeJS คุณจะสามารถใช้ Taquito ได้!

บทช่วยสอนนี้ยังแนะนำเครื่องมือต่างๆ ที่คุณอาจต้องใช้ในการพัฒนา dapps บน Tezos, Beacon SDK เพื่อโต้ตอบกับกระเป๋าเงิน, TzKT API เพื่อรับข้อมูลเพิ่มเติมจากบล็อกเชน เป็นต้น

แม้ว่าบทช่วยสอนนี้จะใช้ Svelte เป็นเฟรมเวิร์กที่เลือก แต่ทักษะที่คุณเรียนรู้นั้นสามารถถ่ายโอนไปยังเฟรมเวิร์กอื่นๆ ได้เนื่องจากอิงตามแนวคิดเดียวกันจำนวนมาก (วงจรชีวิตของคอมโพเนนต์จะคล้ายกันมาก เป็นต้น) มันให้ทุกสิ่งที่คุณต้องการในการสร้าง Dapps ที่น่าทึ่งบน Tezos และฉันอดใจรอไม่ไหวแล้วว่าคุณจะสร้างอะไรต่อไป!