diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index 833bf4fac..38e6c7814 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -326,15 +326,23 @@ const tutorialNavigation = [ href: '/tutorials/build-your-first-app', children: [ { - title: 'Wallets and user tokens', + title: "Introduction", + href: '/tutorials/build-your-first-app', + }, + { + title: 'Part 1: Setting up the application', + href: '/tutorials/build-your-first-app/setting-up-app' + }, + { + title: 'Part 2: Accessing wallets and tokens', href: '/tutorials/build-your-first-app/wallets-tokens', }, { - title: 'Swapping tokens', + title: 'Part 3: Swapping tokens', href: '/tutorials/build-your-first-app/swapping-tokens', }, { - title: 'Adding and removing liquidity', + title: 'Part 4: Adding and removing liquidity', href: '/tutorials/build-your-first-app/adding-removing-liquidity', }, ], diff --git a/src/pages/tutorials/build-your-first-app/adding-removing-liquidity.md b/src/pages/tutorials/build-your-first-app/adding-removing-liquidity.md index 8527b3e62..b07e1afe9 100644 --- a/src/pages/tutorials/build-your-first-app/adding-removing-liquidity.md +++ b/src/pages/tutorials/build-your-first-app/adding-removing-liquidity.md @@ -1,487 +1,274 @@ --- id: adding-removing-liquidity -title: Adding Liquidity -authors: Claude Barde -lastUpdated: 10th July 2023 +title: "Part 4: Adding and removing liquidity" +authors: 'Claude Barde, Tim McMackin' +lastUpdated: 12th September 2023 --- -## Adding Liquidity - -This one is going to be a big one, but a little less involved than swapping tokens! - -The most complex part about adding liquidity to the Liquidity Baking contract is to get the amounts of tokens right! After that, it will be a walk in the park. - -First, let's understand what we are doing here: the LB DEX gives you the ability to provide a pair of tokens (only 2 choices here, XTZ and tzBTC) as liquidity to enable the swapping feature. In exchange, you get SIRS tokens to represent your investment. These tokens increase in value over time, so if you wait long enough, you can make a profit when you remove your liquidity, which will be explained in the next chapter. - -The interface here is going to look a lot like the interface for swapping, with some key differences: - -![AddLiquidity UI](/images/build-your-first-app/add-liquidity-ui.png "Add liquidity UI") - -Like before, we have 2 input fields, but this time, there is no middle button to switch between the 2 tokens and both inputs are editable. - -When inputting a number in one of the fields, the app must calculate the corresponding amount of the other token, as well as the expected amount in SIRS that will be received. - -Now, let's see how all of that is done! - -### Converting the input - -When the user is going to input a number in one of the fields, the input will dispatch a new event to the interface component with the name of the token involved and the amount that was input. This data will be read by the `saveInput` function: - -```typescript= -const saveInput = ev => { - const { token, val }: { token: token; val: number | null } = ev.detail; - ... -} -``` - -Then, we will introduce a condition based on the token because the calculations will be different to convert an amount of XTZ into tzBTC and vice-versa. Let's start with XTZ: - -```typescript= -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 = ""; - } - ... -} -``` - -The condition also includes a check for the value, as there is no need to process it if the value is `null` or `0`. - -The value is cast to a string and stored in the `inputXtz` variable to be used later. The corresponding amount of tzBTC is calculated with the `addLiquidityTokenIn` function, another one of those useful functions to calculate different token amounts for the LB DEX, here it is for reference: - -```typescript= -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; - } -}; -``` - -We check the output of `addLiquidityTokenIn` and we update the `inputTzbtc` variable. - -If the user inputs an amount in tzBTC, the steps will be very similar to calculate the corresponding amount in XTZ: - -```typescript= -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 = ""; - } -} -``` - -We also need to check that the provided value is correct, after what we use the `tokenToXtzXtzOutput` function to get the corresponding amount of XTZ to create a valid pair and provide liquidity: - -```typescript= -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; - } -}; -``` - -Once this is calculated, we store the result in the `inputXtz` variable for later use. - -### Calculating the expected amount of SIRS - -Now, we have to calculate the corresponding amount of SIRS that will be created if `inputXtz` and `inputTzbtc` are provided as parameters to add liquidity. The `addLiquidityLiquidityCreated` function does all the hard work for us: - -```typescript= -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; - } -}; -``` - -This function takes 3 parameters: - -1. the amount of XTZ you want to add as liquidity -2. the current state of the XTZ pool -3. the total amount of liquidity available in the contract (i.e. the SIRS tokens) - -It will output the amount of SIRS created after the transaction. This amount is stored in the `sirsOutput` variable to be displayed in the interface. - -### Sending tokens - -After we calculated all the values we need to add liquidity to the Liquidity Baking contract, it's time to forge the transaction! - -```typescript= -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); - ... - -} -``` - -First, we check that the 3 values we need, the amounts of XTZ, tzBTC, and SIRS are available. If it is the case, we update the UI by switching the `addLiquidityStatus` variable to `TxStatus.Loading` and displaying a simple toast with a message. - -After that, we convert the amount of tzBTC we got into its "real" value, i.e. the value without decimal points as stored in its contract. - -Then, we create the `ContractAbstraction` for the LB DEX and the `ContractAbstraction` for the tzBTC contract, as we will interact with both. - -> _Note: remember, every time your users want to use tzBTC with the LB DEX, the amount of tokens that will be used needs to be approved at the tzBTC contract level, which requires 3 different operations._ - -At this point, you may have guessed that we have to create a batched transaction, but let's do it in a different way from the previous chapter, so you can choose the way you prefer: - -```typescript= -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(); -``` - -In the previous chapter, the batched transaction was created using the `withContractCall` method available on the `batch` method. Here, we will actually pass a parameter to the `batch()` method, an array containing multiple objects that each represent an operation. - -The first operation: - -```typescript= -{ -kind: OpKind.TRANSACTION, -...tzBtcContract.methods.approve(dexAddress, 0).toTransferParams() -} -``` - -is the transaction required to set the amount of approved tzBTC for the LB DEX to zero. - -The second operation: - -```typescript= -{ -kind: OpKind.TRANSACTION, -...tzBtcContract.methods - .approve(dexAddress, tzbtcForLiquidity) - .toTransferParams() -} -``` - -sets the amount of approved tzBTC for the LB DEX contract. - -The third operation: - -```typescript= -{ - kind: OpKind.TRANSACTION, - ...lbContract - .methodsObject - .addLiquidity({ - owner: $store.userAddress, - minLqtMinted: sirsOutput, - maxTokensDeposited: tzbtcForLiquidity, - deadline: calcDeadline() - }) - .toTransferParams(), - amount: +inputXtz -} -``` - -is the actual `addLiquidity` operation to provide the pair of tokens to the contract and receive SIRS tokens in exchange. The entrypoint expects 4 parameters (represented here as an object thanks to the `methodsObject` method): - -1. the address of the account that will receive the SIRS tokens -2. the minimum amount of SIRS tokens expected to be received -3. the amount of tzBTC deposited -4. the deadline - -> _Note: look how the attached amount of tez is passed to the operation as the last property of the operation object. It is important to put it after `.toTransferParams()` or it would be overwritten with the default amount of tez, which is zero._ - -The fourth operation: - -```typescript= -{ - kind: OpKind.TRANSACTION, - ...tzBtcContract.methods.approve(dexAddress, 0).toTransferParams() -} -``` - -resets the allowed amount of tzBTC to be used by the LB DEX to zero. - -Then, just like any other transaction forged through Taquito, you call `.send()` and `.confirmation()` on the operation object to wait for one confirmation. - -Once the transaction is confirmed, you clear the UI before fetching the new balances of XTZ, tzBTC, and SIRS. - -If the transaction failed, you update the UI and provide visual feedback to the users: - -```typescript= -addLiquidityStatus = TxStatus.Error; -store.updateToast(true, "An error has occurred"); -``` - -After all these steps, you can reset the interface to its previous state, maybe the user wants to add more liquidity! - -```typescript= -setTimeout(() => { - addLiquidityStatus = TxStatus.NoTransaction; - store.showToast(false); -}, 3000); -``` - -And that's it! Your users now have the ability to add liquidity to the Liquidity Baking DEX and invest their XTZ and tzBTC. - - - -Removing liquidity from the Liquidity Baking contract is arguably the easiest of all the tasks accomplished by our interface. The interface only needs one input to receive the amount of SIRS that the user wants to unwrap to get XTZ and tzBTC. +As mentioned before, users stake XTZ and tzBTC to the LB contract in a process called "adding liquidity." +Those tokens become the liquidity pool that other users can use to swap tokens. +In this section, you will enhance the application to allow users to: + +- Stake tokens and receive SIRS +- Return the SIRS and receive their XTZ and tzBTC back with interest + +The most difficult part of this process is getting the tokens amounts correct. +Sending the transactions to Tezos is simple compared to the swap transactions. + +The interface for staking tokens looks similar to the interface for swapping, but in this case, both token amounts are editable. +The user enters a token amount in one of the fields and the app calculates the amount of the other token in the other field. +Then, it displays the amount of SIRS tokens that the user receives, as in this picture: + +![The interface for adding liquidity](/images/build-your-first-app/add-liquidity-ui.png "Add liquidity UI") + +## Calculating token amounts + +To set up the code that handles adding and removing liquidity, copy these files from the tutorial repository at : + +- `src/lib/AddLiquidityView.svelte` +- `src/lib/RemoveLiquidityView.svelte` +- `src/lib/SirsStats.svelte` + +When the user enters a number into one of the fields, the app runs these steps: + +1. The input field runs the `saveInput` function in the `AddLiquidityView.svelte` file and passes the name of the selected token and the amount. +This function calculates the amounts for the UI based on the field that the user filled in. +For example, if the user specifies an amount of XTZ, this code calculates the amount of tzBTC and stores it in the `inputTzbtc` variable: + + ```typescript + 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 = ""; + } + // ... + } + ``` + +1. The function calculates the amount of liquidity created by the transaction via the `addLiquidityLiquidityCreated` function and stores it in the `sirsOutput` variable: + + ```typescript + const liquidityCreated = addLiquidityLiquidityCreated({ + xtzIn: val * 10 ** 6, + xtzPool: $store.dexInfo.xtzPool, + totalLiquidity: $store.dexInfo.lqtTotal + }); + if (liquidityCreated) { + sirsOutput = Math.floor(liquidityCreated.toNumber()); + } else { + sirsOutput = 0; + } + ``` + + You can see the code of the `addLiquidityLiquidityCreated` function in the `src/lbUtils.ts` file. + + This function takes 3 parameters: + + - The amount of XTZ to add + - The current state of the XTZ pool + - The total amount of liquidity available in the contract, represented by the SIRS tokens + + The function returns the amount of SIRS that is created after the transaction. + The `saveInput` function stores this amount in the `sirsOutput` variable and shows it on the interface. + +## Sending the transaction + +When the user confirms the transaction to add liquidity, the app runs the `addLiquidity` function: + +1. The function verifies that the token amounts are set and updates the UI to show that the transaction is in progress. + +1. The function creates objects of the `ContractAbstraction` type to represent the LB contract and the contract that manages tzBTC: + + ```typescript + const lbContract = await $store.Tezos.wallet.at(dexAddress); + const tzBtcContract = await $store.Tezos.wallet.at(tzbtcAddress); + ``` + +1. As with the transaction to swap tzBTC to XTZ, the transaction to add liquidity requires multiple steps. +You could use a Taquito batch operation as in the swap function, but to illustrate a different way of bundling the transactions, the `addLiquidity` function passes transactions to the `batch` method as an array: + + ```typescript + 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() + } + ]); + ``` + + This series of transactions is similar to the transactions that swap tokens: + + 1. It calls the LB contract's `approve` entrypoint to set the number of tokens that the contract can take from the wallet to 0. + 1. It calls the LB contract's `approve` entrypoint to set the number of tokens that the contract can take from the wallet to the number of tokens that the user intends to send. + 1. It sends the transaction with the tokens. + 1. It sets the the number of tokens that the contract can take from the wallet back to 0. + + This is the call to the contract's `addLiquidity` entrypoint: + + ```typescript + { + kind: OpKind.TRANSACTION, + ...lbContract + .methodsObject + .addLiquidity({ + owner: $store.userAddress, + minLqtMinted: sirsOutput, + maxTokensDeposited: tzbtcForLiquidity, + deadline: calcDeadline() + }) + .toTransferParams(), + amount: +inputXtz + } + ``` + + It passes these parameters, put in an object by the `methodsObject` method: + + - The address of the account that receives the SIRS tokens + - The minimum amount of SIRS tokens to be received + - The amount of tzBTC deposited + - The deadline + + Finally, it adds the amount of XTZ as the last property of the operation. + This field must be after the `toTransferParams` function or it is overwritten with the default amount, which is zero. + +1. Then the function sends the transaction to Tezos and waits for it to complete: + + ```typescript + const batchOp = await batch.send(); + await batchOp.confirmation(); + ``` + +1. The function clears the UI: + + ```typescript + addLiquidityStatus = TxStatus.Success; + inputXtz = ""; + inputTzbtc = ""; + sirsOutput = 0; + ``` + +1. The function updates the user's balances with the `fetchBalances` function. + +Now the app uses can add liquidity to the Liquidity Baking contract and invest their XTZ and tzBTC. + +## Removing liquidity + +The transaction to remove liquidity requires only the amount of SIRS that the user wants to exchange for tzBTC and XTZ. +Therefore, the UI shows only one input field: ![RemoveLiquidity UI](/images/build-your-first-app/remove-liquidity-ui.png "Remove liquidity UI") -The app will then calculate the corresponding amount of XTZ and tzBTC expected to be received for the amount of SIRS in the input field. - -In the `lbUtils.ts` file, you will find the `removeLiquidityXtzTzbtcOut` function to calculate these amounts: - -```typescript= -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(); - } -``` +The app uses the `removeLiquidityXtzTzbtcOut` function in the `src/lbUtils.ts` file to calculate the amount of XTZ and tzBTC that the user receives: + + ```typescript + 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(); + } + ``` This function takes an object as a parameter with 4 properties: -- `liquidityBurned` -> the amount of SIRS to burn -- `totalLiquidity` -> the total amount of SIRS tokens in the contract -- `xtzPool` -> the total amount of XTZ tokens in the contract -- `tokenPool` -> the total amount of tzBTC tokens in the contract - -If the function has been able to calculate the amounts of XTZ and tzBTC, they are returned in an object, otherwise `null` is returned. After that, those amounts can be displayed in the interface. - -Now, let's see how to interact with the `removeLiquidity` entrypoint of the contract. First, we create a `removeLiquidity` function within our TypeScript code that will be triggered when the user clicks on the `Remove liquidity` button: - -```typescript= -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); - - ... - -}; -``` - -The function starts by checking if there is an amount of SIRS that was input before the remove liquidity action was triggered. If that's the case, the `removeLiquidityStatus` is set to `loading` to update the UI and inform the user that the transaction is getting ready. A toast will also be displayed. - -Next, a `ContractAbstraction` is created for the LB DEX in order to interact with it from Taquito. +- `liquidityBurned`: The amount of SIRS to return and burn +- `totalLiquidity`: The total amount of SIRS tokens in the contract +- `xtzPool`: The total amount of XTZ tokens in the contract +- `tokenPool`: The total amount of tzBTC tokens in the contract -Now, we can forge the actual transaction: +The `removeLiquidity` function in the `src/lib/RemoveLiquidityView.svelte` file creates the transaction by running these steps: -```typescript= -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. It verifies the number of SIRS to return and updates the UI to show that a transaction is pending. -The `removeLiquidity` entrypoint expects 5 parameters: +1. It creates an object of the `ContractAbstraction` that represents the LB contract: -1. `to` -> the account that will receive the XTZ and tzBTC -2. `lqtBurned` -> the amount of SIRS to burn -3. `minXtzWithdrawn` -> the minimum amount of XTZ expected to be received -4. `minTokensWithdrawn` -> the minimum amount of tzBTC expected to be received -5. `deadline` -> just as the other entrypoint, a deadline for the transaction must be provided + ```typescript + const lbContract = await $store.Tezos.wallet.at(dexAddress); + ``` -After the transaction has been emitted, we call `.confirmation()` on the operation object returned by Taquito. +1. It creates the transaction, sends it, and waits for it to complete: -If the transaction was successful, we update the UI and reset the token values to let the user know: + ```typescript + 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(); + ``` -```typescript= -removeLiquidityStatus = TxStatus.Success; -inputSirs = ""; -xtzOutput = 0; -tzbtcOutput = 0; + The `removeLiquidity` entrypoint accepts these parameters: -// 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); -} + - `to`: The account that receives the XTZ and tzBTC + - `lqtBurned`: The amount of SIRS to return and burn + - `minXtzWithdrawn`: The minimum amount of XTZ to receive + - `minTokensWithdrawn`: The minimum amount of tzBTC to receive + - `deadline`: The deadline -store.updateToast(true, "Liquidity successfully removed!"); -``` +1. The function updates the user's balances with the `fetchBalances` function. -If the transaction failed, we also update the UI accordingly: +Now the users can return their SIRS tokens and receive their XTZ and tzBTC tokens. -```typescript= -removeLiquidityStatus = TxStatus.Error; -store.updateToast(true, "An error has occurred"); -``` +## Summary -And that's it, the users have now the possibility to remove SIRS tokens and get XTZ and tzBTC tokens in exchange! +You've made it until the end of this tutorial! 🙂 +To start the application, run `npm run dev` from the command line and open it in a browser. -You've made it until the end of this tutorial 🙂 +You learned: many concepts that are fundamental to developing applications on Tezos and to understanding how Tezos works in general, including: -This very simple app introduced a lot of different concepts that are fundamental to developing applications on Tezos, but also to understanding how Tezos works in general. +- How to use Taquito to develop on Tezos, interact with smart contracts, and use wallet, whether you want to prototype ideas quickly or want to create full-stack decentralized applications. -Taquito is an amazing library to develop on Tezos, whether you want to prototype ideas quickly or want to create full-stack decentralized applications. It provides a main library with all you need to read from the Tezos blockchain, interact with smart contracts and use wallets, and several smaller packages for specific usage, for example, reading token metadata or batching operations. +- How to use the Beacon SDK to interact with wallets. -Whether you want to build a front-end app, a back-end, or even a desktop app, as long as you are using JavaScript/NodeJS, you will be able to use Taquito! +- How to use the TzKT API to get data from the blockchain. -This tutorial also introduced different tools you may need on your journey to developing dapps on Tezos, The Beacon SDK to interact with wallets, the TzKT API to get more data from the blockchain, etc. +Although this tutorial uses Svelte for its framework, the skills you learned are transferrable to other JS/TS frameworks, because many of them use the same concepts, such as similar component lifecycles. -Although this tutorial uses Svelte as its framework of choice, the skills you learned are transferrable to other frameworks as they are based on a lot of the same concepts (the component lifecycles are very similar, etc.) It gives you everything you need to build amazing dapps on Tezos and I can't wait to see what you will build next! +Now you know many of the things that you need to build amazing dApps on Tezos and we can't wait to see what you build next! diff --git a/src/pages/tutorials/build-your-first-app/index.md b/src/pages/tutorials/build-your-first-app/index.md index ae89b8d93..310c08ca0 100644 --- a/src/pages/tutorials/build-your-first-app/index.md +++ b/src/pages/tutorials/build-your-first-app/index.md @@ -1,554 +1,70 @@ --- id: build-your-first-app title: Build your first app on Tezos -authors: Claude Barde -lastUpdated: 10th July 2023 +authors: 'Claude Barde, Tim McMackin' +lastUpdated: 12th September 2023 --- -In this tutorial, you will learn how to set up and create a decentralized web application on Tezos. We will build together an interface for the Liquidity Baking smart contract that will allow us to interact with this DEX and perform different operations, like swapping tokens or providing liquidity. At the same time, you will be introduced to core concepts of building a decentralized application in general, but also specifically on Tezos. +This tutorial shows you how to create a decentralized web application on Tezos that swaps one token for another. +Specifically, this application will be the user-facing web front end for a distributed exchange (DEX) smart contract that performs operations such as swapping tokens. -As the app will be built with [TypeScript](https://www.typescriptlang.org/), a good knowledge of this programming language is required. We will use the [Svelte](https://svelte.dev/) framework to develop the application, no prior knowledge of it is required as it is pretty intuitive to use and I will explain how it works along the way. +You will learn: -As 99% of the dapps in the ecosystem, this app will use [Taquito](https://tezostaquito.io/), a TypeScript library that will provide a much better developer experience to use the Tezos blockchain. +- How to create a web application and import TypeScript libraries that access Tezos +- How to connect to a user's wallet +- How to send a transaction to a smart contract on behalf of a user -## Overview of this tutorial -### Setting up the project -- Installing ViteJS + Svelte -- Installing Tezos packages -- Configuring ViteJS -- Checking that everything works - -### Setting up the app -- File structure -- Configuration -- The `TezosToolkit` instance - -### Setting up the wallet -- Setting up Beacon -- Design considerations (wallet, etc.) - -### Fetching user's balances -- XTZ balance -- tzBTC balance -- SIRIUS balance -- Displaying the balances - -### Swapping XTZ/tzBTC -- Requirements -- UI design -- Calculating minimum tokens out -- Transaction feedback - -### Adding liquidity -- Requirements -- UI design -- Calculating amounts of XTZ and tzBTC - -### Removing liquidity -- Requirements -- UI design +## Prerequisites +This tutorial uses [TypeScript](https://www.typescriptlang.org/), so it will be easier if you are familiar with TypeScript. +You do not need any familiarity with any of the libraries in the tutorial, including [Taquito](https://tezostaquito.io/), a TypeScript library that helps developers access Tezos. ## The Liquidity Baking contract -There is a special contract on Tezos called the **Liquidity Baking** contract. This contract is a decentralized exchange (or DEX) that handles only 3 tokens: **XTZ** (the native token of Tezos), **tzBTC** (a wrapped token to use Bitcoin on Tezos), and **SIRS** (for _Sirius_, the token that represents an equal amount of liquidity in XTZ and tzBTC added to the contract). - -The particularity of this contract is that every time a new block is baked on Tezos, 2.5 XTZ are added to the contract. Users are expected to bring tzBTC in order to keep the DEX liquidity balanced and the price of SIRS stable. - -The contract is also fully public, which means that anybody with a Tezos wallet can interact with it to swap XTZ for tzBTC and vice-versa, provide liquidity or remove it, which is what we are going to do in this tutorial. - -## What are we going to build? - -In this tutorial, we will build a app interface that interacts with the LB contract to swap tokens, add liquidity and remove it. The app will handle different actions: - -- Displaying users' information like their XTZ, tzBTC, and SIRS balance and update them after each transaction -- Connecting and disconnecting the users' wallet -- Displaying wallet information like its connection status and the network it's connected to -- Displaying different interfaces to swap tokens, add and remove liquidity -- Allowing users to swap XTZ for tzBTC and tzBTC for XTZ -- Allowing users to add liquidity by providing XTZ and tzBTC and getting SIRS in exchange -- Allowing users to remove liquidity, i.e. to redeem SIRS tokens and get XTZ and tzBTC tokens in exchange. - -## What tools are we going to use? - -As the decentralized application is ultimately a web app, we will use the following tools to build it: - -- **Svelte** for the JavaScript framework -- **TypeScript** to make our JavaScript code safer and more expressive -- **Sass** as a CSS preprocessor -- **Vite** to bundle the application (pronounced like _veet_) -- **Taquito** to interact with the Tezos blockchain -- **Beacon** and the wrapper library provided by Taquito to use a Tezos wallet - -## Useful links - -- Svelte => https://svelte.dev/ -- TypeScript => https://www.typescriptlang.org/ -- Sass => https://sass-lang.com/ -- Vite => https://vitejs.dev/ -- Taquito => https://tezostaquito.io/ -- Beacon => https://docs.walletbeacon.io/ -- GitHub repo with the app => https://github.com/claudebarde/tezos-dev-portal-tutorial - - -As we are building a web app with the Svelte framework, the steps to set up the project will be very similar to the ones you would follow to set up any other web app. - -In this tutorial, we will make a Svelte SPA, so we won't need SvelteKit, which will also make our life easier. - -The first thing to do is to install Svelte with TypeScript and Vite: - -``` -npm create vite@latest lb-dex -- --template svelte-ts -cd lb-dex -npm install -``` - -Next, we will install all the dependencies we need for the app: - -``` -npm install --save-dev sass -npm install @taquito/taquito @taquito/beacon-wallet -``` - -Sass is a development-only dependency, `@taquito/taquito` is the NPM package for the Taquito library and `@taquito/beacon-wallet` is the NPM package that contains Beacon with some little configuration to make it easier to plug into Taquito. - -There are a couple of other libraries we need to install: - -``` -npm install --save-dev buffer events vite-compatible-readable-stream -``` - -These libraries are required to be able to run Beacon in a Svelte app. We will see down below how to use them. - -Once everything has been installed, we have to set up the right configuration. - -In your `app` folder, you will see the `vite.config.js` file, it's the file that contains the configuration that Vite needs to run and bundle your app. Make the following changes: - -```javascript= -import { defineConfig, mergeConfig } from "vite"; -import path from "path"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; - -export default ({ command }) => { - const isBuild = command === "build"; - - return defineConfig({ - plugins: [svelte()], - define: { - global: {} - }, - build: { - target: "esnext", - commonjsOptions: { - transformMixedEsModules: true - } - }, - server: { - port: 4000 - }, - resolve: { - alias: { - "@airgap/beacon-sdk": path.resolve( - path.resolve(), - `./node_modules/@airgap/beacon-sdk/dist/${ - isBuild ? "esm" : "cjs" - }/index.js` - ), - // polyfills - "readable-stream": "vite-compatible-readable-stream", - stream: "vite-compatible-readable-stream" - } - } - }); -}; -``` - -Here are a few changes we made to the template configuration given by Vite: -- We set `global` to `{}` and we will later provide the `global` object in our HTML file -- We provide a path to the Beacon SDK -- We provide polyfills for `readable-stream` and `stream` - -Once these changes have been done, there is a last step to finish setting up the project: we have to update the HTML file where the JavaScript code will be injected. - -Here is what you should have: - -```html= - - - - - - - - - Liquidity Baking DEX - - - - - -``` - -In the first `script` tag, we set the `global` variable to `globalThis`. Then, in the second `script` tag with a `module` type, we import `Buffer` from the `buffer` library and add it to the `window` global object. - -> *Note: this configuration is required to run the Beacon SDK with a Vite app. Taquito works completely out of the box and doesn't require any settings.* - -Once we updated the configuration in the `vite.config.js` file and in the `index.html` file, our project is successfully set up! You can run `npm run dev` in your terminal at the root of the project to check that everything works properly, the app should be running on `http://localhost:4000` - -Now, let's start writing some code and setting up the app! - -As we are building a web app with the Svelte framework, the steps to set up the project will be very similar to the ones you would follow to set up any other web app. - -In this tutorial, we will make a Svelte SPA, so we won't need SvelteKit, which will also make our life easier. - -The first thing to do is to install Svelte with TypeScript and Vite: - -``` -npm create vite@latest lb-dex -- --template svelte-ts -cd lb-dex -npm install -``` - -Next, we will install all the dependencies we need for the app: +This tutorial uses a special contract on Tezos called the *Liquidity Baking* (LB) contract. +The LB contract is a decentralized exchange (or DEX) that handles only 3 tokens: -``` -npm install --save-dev sass -npm install @taquito/taquito @taquito/beacon-wallet -``` +- **XTZ**, the native token of Tezos +- **tzBTC**, a wrapped token that represents Bitcoin on Tezos +- **SIRS**, short for _Sirius_, a token that represents an equal amount of XTZ and tzBTC tokens -Sass is a development-only dependency, `@taquito/taquito` is the NPM package for the Taquito library and `@taquito/beacon-wallet` is the NPM package that contains Beacon with some little configuration to make it easier to plug into Taquito. +Like a physical currency exchange at an airport or train station, the LB contract maintains a *liquidity pool* of the tokens that it exchanges. +Users can deposit XTZ and receive tzBTC from the pool or vice versa. -There are a couple of other libraries we need to install: +Tezos automatically deposits XTZ into the contract, but users must provide the tzBTC for the liquidity pool. +Any user can deposit an equal amount of XTZ and tzBTC tokens to the pool, which is called "adding liquidity." +Then, the LB contract provides SIRS tokens as proof of that deposit. +Later, users can return the SIRS and withdraw their XTZ and tzBTC with interest. -``` -npm install --save-dev buffer events vite-compatible-readable-stream -``` +## The tutorial application -These libraries are required to be able to run Beacon in a Svelte app. We will see down below how to use them. +In this tutorial, you build a web application that allows users to swap tokens via the LB contract and to add and withdraw tokens from its liquidity pool. +The token swap portion of the application looks like this: -Once everything has been installed, we have to set up the right configuration. +![Swap UI](/images/build-your-first-app/swap-ui.png "Swap UI") -In your `app` folder, you will see the `vite.config.js` file, it's the file that contains the configuration that Vite needs to run and bundle your app. Make the following changes: +The application can perform many individual actions, including: -```javascript= -import { defineConfig, mergeConfig } from "vite"; -import path from "path"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; - -export default ({ command }) => { - const isBuild = command === "build"; - - return defineConfig({ - plugins: [svelte()], - define: { - global: {} - }, - build: { - target: "esnext", - commonjsOptions: { - transformMixedEsModules: true - } - }, - server: { - port: 4000 - }, - resolve: { - alias: { - "@airgap/beacon-sdk": path.resolve( - path.resolve(), - `./node_modules/@airgap/beacon-sdk/dist/${ - isBuild ? "esm" : "cjs" - }/index.js` - ), - // polyfills - "readable-stream": "vite-compatible-readable-stream", - stream: "vite-compatible-readable-stream" - } - } - }); -}; -``` - -Here are a few changes we made to the template configuration given by Vite: -- We set `global` to `{}` and we will later provide the `global` object in our HTML file -- We provide a path to the Beacon SDK -- We provide polyfills for `readable-stream` and `stream` - -Once these changes have been done, there is a last step to finish setting up the project: we have to update the HTML file where the JavaScript code will be injected. - -Here is what you should have: - -```html= - - - - - - - - - Liquidity Baking DEX - - - - - -``` - -In the first `script` tag, we set the `global` variable to `globalThis`. Then, in the second `script` tag with a `module` type, we import `Buffer` from the `buffer` library and add it to the `window` global object. - -> *Note: this configuration is required to run the Beacon SDK with a Vite app. Taquito works completely out of the box and doesn't require any settings.* - -Once we updated the configuration in the `vite.config.js` file and in the `index.html` file, our project is successfully set up! You can run `npm run dev` in your terminal at the root of the project to check that everything works properly, the app should be running on `http://localhost:4000` - -Now, let's start writing some code and setting up the app! - -If you've made it so far and your app is running on `http://localhost:4000`, congratulations! - -Now, we have to set up the app in order to use Taquito and Beacon. - -### File structure - -The entrypoint of every Svelte app is a file called `App.svelte`, this is where you will import all your components to be bundled together into your final app. The file structure of our project looks like this: - -``` -- src - - assets - - svelte.png - - lib - - AddLiquidityView.svelte - - Interface.svelte - - RemoveLiquidity.svelte - - Sidebar.svelte - - SirsStats.svelte - - SwapView.svelte - - Toast.svelte - - UserInput.svelte - - UserStats.svelte - - Wallet.svelte - - styles - - index.scss - - settings.scss - - App.svelte - - config.ts - - lbUtils.ts - - main.ts - - store.ts - - types.ts - - utils.ts -- index.html -- svelte.config.js -- tsconfig.json -- vite.config.js -``` - -Let's see what each of these elements does: - -- **assets** -> contains the favicon (here, this is the default Svelte favicon, but you can choose another one) -- **lib** -> contains the different components that will make up our interface, here is what each does: - - `SwapView.svelte`: the interface to swap XTZ and tzBTC tokens - - `AddLiquidityView.svelte`: the interface to add liquidity to the LB DEX - - `RemoveLiquidity.svelte`: the interface to remove liquidity from the LB DEX - - `Interface.svelte`: the higher-order component to hold the different views to interact with the LB DEX - - `Sidebar.svelte`: the component to navigate between the different interfaces and to connect or disconnect the wallet - - `SirsStats.svelte`: the component to display the amount of XTZ, tzBTC, and SIRS present in the contract - - `Toast.svelte`: a simple component to display the progression of the transactions and other messages when interacting with the contract - - `UserInput.svelte`: a utility component to make it easier to interact and control input fields - - `UserStats.svelte`: the component to display the user's balance in XTZ, tzBTC, and SIRS - - `Wallet.svelte`: the component to manage wallet interactions -- **styles** -> contains the SASS files to style different elements of our interface -- **App.svelte** -> the entrypoint of the application -- **config.ts** -> different immutable values needed for the application and saved in a separate file for convenience -- **lbUtils.ts** -> different methods to calculate values needed to interact with the Liquidity Baking contract -- **main.ts** -> this is where the JavaScript for the app is bundled before being injected into the HTML file -- **store.ts** -> a file with a [Svelte store](https://svelte.dev/tutorial/writable-stores) to handle the app state -- **types.ts** -> custom TypeScript types -- **utils.ts** -> different utility methods - -The first thing to do is to import our styles into the `main.ts` file: - -```typescript= -import App from './App.svelte' -import "./styles/index.scss"; - -const app = new App({ - target: document.body -}) - -export default app -``` - -Svelte uses SASS by default, so there is no configuration to do for that. - -> _Note: I also like to target the `body` tag to inject the HTML produced by JavaScript instead of a `div` inside the `body`, but that's a personal choice and you are free to use a `div` instead_ - -Before continuing, this is what a Svelte file looks like: - -```html= - - - - -... your HTML code -``` - -Svelte components are fully contained, which means that the style that you apply inside a component doesn't leak into the other components of your app. The style that we want to share among different components will be written in the `index.scss` file. - -There is a `script` tag with a `lang` attribute set to `ts` for TypeScript, a `style` tag with a `lang` attribute set to `scss` for SASS, and the rest of the code in the file will be interpreted as HTML. - -### Configuring the app - -Now, let's set up different things in our `App.svelte` file. - -The HTML part is just going to put all the higher-order components together: - -```html= -
- - {#if $store.Tezos && $store.dexInfo} - - - {:else} -
Loading
- {/if} -
-``` - -The interface will change after different elements are available to the app, mostly, the data about the liquidity pools from the liquidity baking contract. - -The SASS part will import different settings and apply styling to the `main` tag: - -```scss= -@import "./styles/settings.scss"; - -main { - display: grid; - grid-template-columns: 250px 1fr; - gap: $padding; - padding: $padding; - height: calc(100% - (#{$padding} * 2)); -} - -@media screen and (max-height: 700px) { - main { - padding: 0px; - height: 100%; - } -} -``` - -Now, the TypeScript part. First, you import the libraries and components we need: - -```typescript= -import { onMount } from "svelte"; -import { TezosToolkit } from "@taquito/taquito"; -import store from "./store"; -import { rpcUrl, dexAddress } from "./config"; -import Sidebar from "./lib/Sidebar.svelte"; -import Interface from "./lib/Interface.svelte"; -import Toast from "./lib/Toast.svelte"; -import type { Storage } from "./types"; -import { fetchExchangeRates } from "./utils"; -``` - -- `onMount` is a method exported by Svelte that will run some code when the component mounts (more on that below) -- `TezosToolkit` is the class that gives you access to all the features of Taquito -- `store` is a Svelte feature to manage the state of the app -- From the `config.ts` file, we import `rpcUrl` (the URL of the Tezos RPC node) and `dexAddress`, the address of the Liquidity Baking contract -- `Storage` is a custom type that represents the signature type of the LB DEX storage -- `fetchExchangeRates` is a function to fetch the exchange rates of XTZ and tzBTC (more on that below) - -Next, we use `onMount` to set up the state of the app: - -```typescript= -onMount(async () => { - const Tezos = new TezosToolkit(rpcUrl); - store.updateTezos(Tezos); - const contract = await Tezos.wallet.at(dexAddress); - const storage: Storage | undefined = await contract.storage(); - - if (storage) { - store.updateDexInfo({ ...storage }); - } +- Connecting and disconnecting users' wallets +- Displaying users' information, including their XTZ, tzBTC, and SIRS balance +- Displaying different interfaces to swap tokens and add and remove liquidity +- Allowing users to swap XTZ for tzBTC and tzBTC for XTZ +- Allowing users to add liquidity by providing XTZ and tzBTC and receiving SIRS +- Allowing users to remove liquidity by providing SIRS and receiving XTZ and tzBTC - // fetches XTZ and tzBTC prices - const res = await fetchExchangeRates(); - if (res) { - store.updateExchangeRates([ - { token: "XTZ", exchangeRate: res.xtzPrice }, - { token: "tzBTC", exchangeRate: res.tzbtcPrice } - ]); - } else { - store.updateExchangeRates([ - { token: "XTZ", exchangeRate: null }, - { token: "tzBTC", exchangeRate: null } - ]); - } - }); -``` +The application is based on JavaScript/TypeScript, so it uses several JS and TS-based tools to build and package the application: -The first thing to do is to create an instance of the `TezosToolkit` by passing the URL of the RPC node we want to interact with. In general, you want to have a single instance of the `TezosToolkit` in order to keep the same configuration across all your app components, this is why we save it in the `store` with the `updateTezos` method. +- **[Svelte](https://svelte.dev/)** for the JavaScript framework +- **[TypeScript](https://www.typescriptlang.org/)** to make the JavaScript code safer and more expressive +- **[Sass](https://sass-lang.com/)** as a CSS preprocessor +- **[Vite](https://vitejs.dev/)** (pronounced like _veet_) to bundle the application and provide the libraries to the user's browser -After that, we want to fetch the storage of the LB DEX to get the amounts of XTZ, tzBTC, and SIRS in the contract. We create a `ContractAbstraction`, an instance provided by Taquito with different properties and methods that are useful to work with Tezos smart contracts. -From the `ContractAbstraction`, we can call the `storage` method that returns a JavaScript object that represents the storage of the given contract. We then pass the storage to the `updateDexInfo` method present on the `store` to update this data and display them to the user. +Also, to access the user's wallet and run transactions on Tezos, the application uses these libraries: -To finish, we need to fetch the exchange rates for XTZ and tzBTC to make the conversions required by this kind of app. The `utils.ts` file contains a function that will help us here: +- **[Taquito](https://tezostaquito.io/)** to interact with the Tezos blockchain +- **[Beacon](https://docs.walletbeacon.io/)** to access users' wallets -```typescript= -export const fetchExchangeRates = async (): Promise<{ - tzbtcPrice: number; - xtzPrice: number; -} | null> => { - const query = ` - query { - overview { xtzUsdQuote }, - token(id: "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn") { price } - } - `; - const res = await fetch(`https://analytics-api.quipuswap.com/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - query - }) - }); - if (res.status === 200) { - const resData = await res.json(); - let xtzPrice = resData?.data?.overview?.xtzUsdQuote; - let tzbtcPrice = resData?.data?.token?.price; - // validates the 2 values - if (xtzPrice && tzbtcPrice) { - xtzPrice = +xtzPrice; - tzbtcPrice = +tzbtcPrice; - if (!isNaN(xtzPrice) && !isNaN(tzbtcPrice)) { - // tzBTC price is given in XTZ by the API - tzbtcPrice = tzbtcPrice * xtzPrice; - return { tzbtcPrice, xtzPrice }; - } - } else { - return null; - } - } else { - return null; - } -}; -``` +You can see the completed tutorial application here: https://github.com/claudebarde/tezos-dev-portal-tutorial -We use the [QuipuSwap GraphQL API](https://analytics-api.quipuswap.com/graphql) to fetch these exchange rates. After the exchange rates are received, we parse the response from the API and validate the price given for XTZ and tzBTC. These prices are then returned by the function and we can save them in the store. The exchange rates are used, for example, to calculate the total value in USD locked in the contract. +When you're ready, move to the next section to begin setting up the application. diff --git a/src/pages/tutorials/build-your-first-app/setting-up-app.md b/src/pages/tutorials/build-your-first-app/setting-up-app.md new file mode 100644 index 000000000..82bfaa6f8 --- /dev/null +++ b/src/pages/tutorials/build-your-first-app/setting-up-app.md @@ -0,0 +1,660 @@ +--- +id: setting-up-app +title: "Part 1: Setting up the application" +authors: 'Claude Barde, Tim McMackin' +lastUpdated: 8th September 2023 +--- + +You can access Tezos through any JavaScript framework. +This tutorial uses the Svelte framework, and the following steps show you how to start a Svelte application and add the Tezos-related dependencies. +If you are familiar with Svelte, note that this application includes its own Svelte SPA, so it does not require SvelteKit. + +## Setting up the app + +1. Run these commands to install Svelte with TypeScript and Vite: + + ```bash + npm create vite@latest lb-dex -- --template svelte-ts + cd lb-dex + npm install + ``` + +1. Install Sass and the Tezos-related dependencies: + + ```bash + npm install --save-dev sass + npm install @taquito/taquito @taquito/beacon-wallet @airgap/beacon-sdk + ``` + +1. Install the `buffer`, `events`, and `vite-compatible-readable-stream` libraries: + + ```bash + npm install --save-dev buffer events vite-compatible-readable-stream + ``` + +1. Update the `vite.config.js` file to the following code: + + ```javascript + import { defineConfig, mergeConfig } from "vite"; + import path from "path"; + import { svelte } from "@sveltejs/vite-plugin-svelte"; + + export default ({ command }) => { + const isBuild = command === "build"; + + return defineConfig({ + plugins: [svelte()], + define: { + global: {} + }, + build: { + target: "esnext", + commonjsOptions: { + transformMixedEsModules: true + } + }, + server: { + port: 4000 + }, + resolve: { + alias: { + "@airgap/beacon-sdk": path.resolve( + path.resolve(), + `./node_modules/@airgap/beacon-sdk/dist/${ + isBuild ? "esm" : "cjs" + }/index.js` + ), + // polyfills + "readable-stream": "vite-compatible-readable-stream", + stream: "vite-compatible-readable-stream" + } + } + }); + }; + ``` + + This updated file includes these changes to the default Vite configuration: + + - It sets the `global` object to `{}` so the application can provide the value for this object in the HTML file + - It includes the a path to the Beacon SDK + - It provides polyfills for `readable-stream` and `stream` + +1. Update the default HTML file, `index.html`, to the following code: + + ```html + + + + + + + + + Liquidity Baking DEX + + + + + + ``` + + This updated file sets the `global` variable to `globalThis` and adds a buffer object to the window. + The Beacon SDK requires this configuration to run in a Vite app. + +## File structure + +The final structure of the tutorial application will look like this: + +``` +- src + - assets + - svelte.png + - lib + - AddLiquidityView.svelte + - Interface.svelte + - RemoveLiquidity.svelte + - Sidebar.svelte + - SirsStats.svelte + - SwapView.svelte + - Toast.svelte + - UserInput.svelte + - UserStats.svelte + - Wallet.svelte + - styles + - index.scss + - settings.scss + - App.svelte + - config.ts + - lbUtils.ts + - main.ts + - store.ts + - types.ts + - utils.ts +- index.html +- svelte.config.js +- tsconfig.json +- vite.config.js +``` + +Here are descriptions for each of these files: + +- **assets** -> Contains the favicon and other static files such as images for the application +- **lib** -> Contains the components that make up the app interface: + - `SwapView.svelte`: The interface to swap XTZ and tzBTC tokens + - `AddLiquidityView.svelte`: The interface to add liquidity to the LB DEX + - `RemoveLiquidity.svelte`: The interface to remove liquidity from the LB DEX + - `Interface.svelte`: The higher-order component that contains the different views to interact with the LB DEX + - `Sidebar.svelte`: The component that navigates between the different interfaces and that hosts the button to connect or disconnect the wallet + - `SirsStats.svelte`: The component to display the amount of XTZ, tzBTC, and SIRS present in the LB contract + - `Toast.svelte`: A simple component to display the progress of the transactions and other messages + - `UserInput.svelte`: A utility component to make it easier to interact with input fields + - `UserStats.svelte`: The component to display the user's balance in XTZ, tzBTC, and SIRS + - `Wallet.svelte`: The component that manages wallet interactions +- **styles** -> Contains the Sass files to style different elements of our interface +- **App.svelte** -> The entrypoint of the application, which contains the components that are bundled into the final application +- **config.ts** -> Constants for the application +- **lbUtils.ts** -> Methods to calculate values needed to interact with the LB contract, such as calculating the amount of output tokens +- **main.ts** -> Where the JavaScript for the app is bundled before being injected into the HTML file +- **store.ts** -> A file with a [Svelte store](https://svelte.dev/tutorial/writable-stores) to handle the app state +- **types.ts** -> Custom TypeScript types +- **utils.ts** -> Utility methods + +## Setting up the style sheets + +Svelte uses Sass, which allows you to create powerful and simple CSS style sheets. +These steps set up the style sheets for the application: + +1. Create a file in the `src/styles` folder (creating the folder if necessary) named `index.scss` and paste in this code: + + ```css + @import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap"); + @import "./settings.scss"; + + html, + body { + height: 100%; + width: 100%; + padding: 0px; + margin: 0px; + font-family: "Montserrat", sans-serif; + font-size: $font-size; + background-color: $honeydew; + color: $prussian-blue; + } + + button { + cursor: pointer; + padding: calc(#{$padding} / 2); + outline: none; + transition: 0.3s; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + + &.primary { + border: solid 2px $celadon-blue; + border-radius: 5px; + background-color: $celadon-blue; + color: white; + font-family: inherit; + font-size: 0.9rem; + + &:hover { + transform: translateY(-2px); + } + + &:disabled { + text-decoration: line-through; + } + } + + &.wallet-button { + align-items: center; + appearance: none; + background-color: #fcfcfd; + border-radius: 4px; + border-width: 0; + box-shadow: rgba(45, 35, 66, 0.4) 0 2px 4px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset; + box-sizing: border-box; + color: inherit; + cursor: pointer; + display: inline-flex; + justify-content: center; + height: 48px; + line-height: 1; + list-style: none; + overflow: hidden; + padding: 0px 16px; + position: relative; + text-align: center; + text-decoration: none; + transition: box-shadow 0.15s, transform 0.15s; + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; + white-space: nowrap; + will-change: box-shadow, transform; + font-size: inherit; + + &:focus { + box-shadow: #d6d6e7 0 0 0 1.5px inset, rgba(45, 35, 66, 0.4) 0 2px 4px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset; + } + + &:hover { + box-shadow: rgba(45, 35, 66, 0.4) 0 4px 8px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset; + transform: translateY(-2px); + } + + &:active { + box-shadow: #d6d6e7 0 3px 7px inset; + transform: translateY(2px); + } + } + + &.sidebar-button { + background-color: transparent; + border: solid 3px transparent; + font-size: inherit; + font-family: inherit; + color: inherit; + margin: 10px; + border-radius: 4px; + + &:hover { + background-color: $powder-blue; + } + + &.active { + border-color: $powder-blue; + } + } + + &.transparent { + background-color: transparent; + border: none; + padding: 0px; + margin: 0px; + color: inherit; + } + + &:disabled { + cursor: not-allowed; + + &:hover { + transform: translateY(0px); + } + } + } + + h1 { + font-size: 2rem; + font-weight: bold; + margin: 0px; + margin-bottom: $padding; + } + + .input-with-logo { + img { + height: 32px; + width: 32px; + } + input { + height: 28px; + padding: 5px 10px; + border: solid 2px transparent; + border-radius: $std-border-radius; + outline: none; + background-color: darken($honeydew, 8); + transition: 0.3s; + font-size: inherit; + color: inherit; + + &:focus { + border-color: $powder-blue; + } + + &.error { + border-color: $imperial-red; + } + } + + .input-with-logo__input { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + padding: 5px 0px; + } + + .input-with-logo__max { + font-size: 0.8rem; + color: inherit; + } + &.left-logo .input-with-logo__max { + float: left; + padding-left: 10px; + } + &.right-logo .input-with-logo__max { + float: right; + padding-right: 10px; + } + } + + .container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; + border: solid 3px darken($powder-blue, 10); + border-radius: $std-border-radius; + padding: calc(#{$padding} * 3); + margin: 20px; + background-color: $powder-blue; + } + + @media screen and (max-height: 700px) { + .container { + padding: 15px; + } + } + + .spinner { + display: inline-block; + position: relative; + width: $font-size; + height: $font-size; + } + .spinner div { + box-sizing: border-box; + display: block; + position: absolute; + width: $font-size; + height: $font-size; + border: 2px solid #fff; + border-radius: 50%; + animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: #fff transparent transparent transparent; + } + .spinner div:nth-child(1) { + animation-delay: -0.45s; + } + .spinner div:nth-child(2) { + animation-delay: -0.3s; + } + .spinner div:nth-child(3) { + animation-delay: -0.15s; + } + @keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + ``` + +1. Create a file in the `src/styles` folder named `settings.scss` and paste in this code: + + ```css + /* Color palette: https://coolors.co/palette/e63946-f1faee-a8dadc-457b9d-1d3557 */ + + $imperial-red: rgba(230, 57, 70, 1); + $honeydew: rgba(241, 250, 238, 1); + $powder-blue: rgba(168, 218, 220, 1); + $celadon-blue: rgba(69, 123, 157, 1); + $prussian-blue: rgba(29, 53, 87, 1); + + $font-size: 18px; + $padding: 20px; + $std-border-radius: 10px; + ``` + +1. In the `src/main.ts` file, import the style sheets by replacing the default code of the file with this code: + + ```typescript + import App from './App.svelte' + import "./styles/index.scss"; + + const app = new App({ + target: document.body + }) + + export default app + ``` + + This code targets the `body` tag to inject the HTML produced by JavaScript instead of a `div` tag inside the `body` tag as Svelte apps do by default. + Your applications can target any tag on the page. + +## Configuring Svelte + +Svelte files include several different types of code in a single file. +The files you will create have separate sections for TypeScript, Sass, and HTML code, as in this example: + +```html + + + + +
+ +
+``` + +Svelte components are fully contained, which means that the style and JS/TS code that you apply inside a component doesn't leak into the other components of your app. +Styles and scripts that are shared among components typically go in the `src/styles` and `scripts` or `src/scripts` folders. + +Follow these steps to set up the `src/App.svelte` file, which is the container for the other Svelte components: + +1. In the `App.svelte` file, replace the default `
` section with this code: + + ```html +
+ + {#if $store.Tezos && $store.dexInfo} + + + {:else} +
Loading
+ {/if} +
+ ``` + + The interface will change after different elements are available to the app, mostly, the data about the liquidity pools from the liquidity baking contract. + +1. Replace the default ` + ``` + +1. Remove the default TypeScript section and replace it with this code, which imports the libraries and components that the app uses: + + ```html + + ``` + + You will add these imported files later, so your IDE may show errors for them now. + The imports include these elements: + + - `onMount`: A method exported by Svelte that runs code when the component mounts + - `TezosToolkit`: The class that gives you access to all the features of Taquito + - `store`: A Svelte feature that manages the state of the app + - `rpcUrl` and `dexAddress`: The URL of the Tezos RPC node and the address of the LB contract + - `Storage`: A custom type that represents the signature type of the LB DEX storage + - `fetchExchangeRates`: A function that fetches the exchange rates of XTZ and tzBTC + +1. In the ` + ``` + + The `connectWallet` function creates a `BeaconWallet` object that represents the user's wallet or if the wallet is already connected, retrieves the connection from the store. + It provides a name for the app, which appears in the wallet UI when it asks the user to allow the connection. + It also includes the network to use, such as the Tezos main network or test network. + Then it stores or updates the wallet object in the Svelte store so other parts of the application can use it. + + The Beacon SDK keeps track of live connections in the store, so if a user has connected to your app before and returns later, their wallet is connected automatically. + + The wallet object also provides a `client` property, which allows you to retrieve the wallet address and token balances and save them to the store so the app can display them on the interface. + + This code uses a custom type named `TezosAccountAddress`, which validates Tezos addresses for implicit accounts. + Tezos addresses start with `tz1`, `tz2`, or `tz3`, so the type checks addresses for these strings. + Its code looks like this: + + ```typescript + type TezosAccountAddress = tz${"1" | "2" | "3"}${string} + ``` + + TypeScript raises a warning if you try to use a string that doesn't match this pattern. + +1. Add the following code to the `