diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e4bfac5..572859565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New CSpell dictionaries: TVM instructions and adjusted list of Fift words: PR [#881](https://github.com/tact-lang/tact/pull/881) - Docs: the `description` property to the frontmatter of the each page for better SEO: PR [#916](https://github.com/tact-lang/tact/pull/916) - Docs: Google Analytics tags per every page: PR [#921](https://github.com/tact-lang/tact/pull/921) -- Docs: Added NFTs cookbook: PR [#958](https://github.com/tact-lang/tact/pull/958) +- Docs: added NFTs cookbook: PR [#958](https://github.com/tact-lang/tact/pull/958) - Ability to specify a compile-time method ID expression for getters: PR [#922](https://github.com/tact-lang/tact/pull/922) and PR [#932](https://github.com/tact-lang/tact/pull/932) - Destructuring of structs and messages: PR [#856](https://github.com/tact-lang/tact/pull/856), PR [#964](https://github.com/tact-lang/tact/pull/964), PR [#969](https://github.com/tact-lang/tact/pull/969) - Docs: automatic links to Web IDE from all code blocks: PR [#994](https://github.com/tact-lang/tact/pull/994) @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `replace` and `replaceGet` methods for the `Map` type: PR [#941](https://github.com/tact-lang/tact/pull/941) - Utility for logging errors in code that was supposed to be unreachable: PR [#991](https://github.com/tact-lang/tact/pull/991) - Docs: `preloadRef` method for the `Slice` type: PR [#1044](https://github.com/tact-lang/tact/pull/1044) +- Docs: added DeDust cookbook: PR [#954](https://github.com/tact-lang/tact/pull/954) ### Changed diff --git a/docs/src/content/docs/cookbook/dexes/dedust.mdx b/docs/src/content/docs/cookbook/dexes/dedust.mdx index 9c95527f3..b55d6739a 100644 --- a/docs/src/content/docs/cookbook/dexes/dedust.mdx +++ b/docs/src/content/docs/cookbook/dexes/dedust.mdx @@ -7,8 +7,438 @@ sidebar: [DeDust](https://dedust.io) is a decentralized exchange (DEX) and automated market maker (AMM) built natively on [TON Blockchain](https://ton.org) and [DeDust Protocol 2.0](https://docs.dedust.io/reference/tlb-schemes). DeDust is designed with a meticulous attention to user experience (UX), gas efficiency, and extensibility. -:::danger[Not implemented] +Before going further, familiarize yourself with the following: - This page is a stub until it gets new content in [#146](https://github.com/tact-lang/tact-docs/issues/146). +* [Receiving messages](/book/receive/) +* [Sending messages](/book/send/) +* [Fungible Tokens (Jettons)](/cookbook/jettons/) +* [DeDust Docs: Concepts](https://docs.dedust.io/docs/concepts) + +## Swaps + +Read more about swaps in the [DeDust documentation](https://docs.dedust.io/docs/swaps). + +:::caution + + It's important to ensure that contracts are deployed. Sending funds to an inactive contract could result in irretrievable loss. + +::: + +All kinds of swaps use `SwapStep{:tact}` and `SwapParams{:tact}` structures: + +```tact +/// https://docs.dedust.io/reference/tlb-schemes#swapstep +struct SwapStep { + // The pool that will do the swapping, i.e. pairs like TON/USDT or USDT/DUST + poolAddress: Address; + + // A kind of swap to make, can only be 0 as of now + kind: Int as uint1 = 0; + + // Minimum output of the swap + // If the actual value is less than specified, the swap will be rejected + limit: Int as coins = 0; + + // Reference to the next step, which can be used for multi-hop swaps + // The type here is actually `SwapStep?`, + // but specifying recursive types isn't allowed in Tact yet + nextStep: Cell?; +} + +/// https://docs.dedust.io/reference/tlb-schemes#swapparams +struct SwapParams { + // Specifies a deadline for the swap to reject the swap coming to the pool late + // Accepts the number of seconds passed since the UNIX Epoch + // Defaults to 0, which removes the deadline + deadline: Int as uint32 = 0; + + // Specifies an address where funds will be sent after the swap + // Defaults to `null`, which makes the swap use the sender's address + recipientAddress: Address? = null; + + // Referral address, required for the referral program of DeDust + // Defaults to `null` + referralAddress: Address? = null; + + // Custom payload that will be attached to the fund transfer upon a successful swap + // Defaults to `null` + fulfillPayload: Cell? = null; + + // Custom payload that will be attached to the fund transfer upon a rejected swap + // Defaults to `null` + rejectPayload: Cell? = null; +} +``` + +### Swap Toncoin for any Jetton + +:::note +The guides below use the [Jetton vault](https://docs.dedust.io/docs/concepts#vault). To obtain its address for your Jetton, refer to [this guide](https://docs.dedust.io/docs/swaps#step-1-find-the-vault-scale). +::: + +```tact +/// https://docs.dedust.io/reference/tlb-schemes#message-swap +message(0xea06185d) NativeSwap { + // Unique identifier used to trace transactions across multiple contracts + // Defaults to 0, which means we don't mark messages to trace their chains + queryId: Int as uint64 = 0; + + // Toncoin amount for the swap + amount: Int as coins; + + // Inlined fields of SwapStep Struct + poolAddress: Address; + kind: Int as uint1 = 0; + limit: Int as coins = 0; + nextStep: SwapStep? = null; + + // Set of parameters relevant for the whole swap + swapParams: SwapParams; +} + +// Let's say `swapAmount` is `ton("0.1")`, which is 10000000 nanoToncoins +fun swapToncoinForUSDT(swapAmount: Int) { + send(SendParameters{ + // Address of TON vault to send the message to + to: address("EQDa4VOnTYlLvDJ0gZjNYm5PXfSmmtL6Vs6A_CZEtXCNICq_"), + // Amount to swap plus a trade fee + value: swapAmount + ton("0.2"), + body: NativeSwap{ + amount: swapAmount, + // Address of the swap pool, which is the TON/USDT pair in this case + poolAddress: address("EQA-X_yo3fzzbDbJ_0bzFWKqtRuZFIRa1sJsveZJ1YpViO3r"), + // Set of parameters relevant for the whole swap + swapParams: SwapParams{}, // use defaults + }.toCell(), + }); +} + +// +// Helper Structs described earlier on this page +// + +struct SwapStep { + poolAddress: Address; + kind: Int as uint1 = 0; + limit: Int as coins = 0; + nextStep: Cell?; +} + +struct SwapParams { + deadline: Int as uint32 = 0; + recipientAddress: Address? = null; + referralAddress: Address? = null; + fulfillPayload: Cell? = null; + rejectPayload: Cell? = null; +} +``` + +### Swap a Jetton for another Jetton or Toncoin + +```tact +/// https://docs.dedust.io/reference/tlb-schemes#message-swap-1 +message(0xe3a0d482) JettonSwapPayload { + // Inlined fields of SwapStep Struct + poolAddress: Address; + kind: Int as uint1 = 0; + limit: Int as coins = 0; + nextStep: SwapStep? = null; + + // Set of parameters relevant for the whole swap + swapParams: SwapParams; +} + +/// NOTE: To calculate and provide Jetton wallet address for the target user, +/// make sure to check links after this code snippet +fun swapJetton(targetJettonWalletAddress: Address) { + send(SendParameters{ + to: targetJettonWalletAddress, + value: ton("0.3"), + body: JettonTransfer{ + // Unique identifier used to trace transactions across multiple contracts + // Set to 0, which means we don't mark messages to trace their chains + queryId: 0, + // Jetton amount for the swap + amount: 10, // NOTE: change to yours + // Address of the Jetton vault to the send message to + destination: address("EQAYqo4u7VF0fa4DPAebk4g9lBytj2VFny7pzXR0trjtXQaO"), + // Where to return the exceeding funds + responseDestination: myAddress(), + forwardTonAmount: ton("0.25"), + forwardPayload: JettonSwapPayload{ + // Address of the swap pool, which is the TON/USDT pair in this case + poolAddress: address("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"), + // Set of parameters relevant for the whole swap + swapParams: SwapParams{}, // use defaults + }.toCell(), + }.toCell(), + }); +} + +// +// Helper Structs described earlier on this page +// + +struct SwapStep { + poolAddress: Address; + kind: Int as uint1 = 0; + limit: Int as coins = 0; + nextStep: Cell?; +} + +struct SwapParams { + deadline: Int as uint32 = 0; + recipientAddress: Address? = null; + referralAddress: Address? = null; + fulfillPayload: Cell? = null; + rejectPayload: Cell? = null; +} + +// +// Messages from the Jetton standard +// + +message(0xf8a7ea5) JettonTransfer { + queryId: Int as uint64; + amount: Int as coins; + destination: Address; + responseDestination: Address; + customPayload: Cell? = null; + forwardTonAmount: Int as coins; + forwardPayload: Cell?; // slightly adjusted +} +``` + +:::note[Useful links:] + + [Retrieving Jetton wallet address in TON Docs](https://docs.ton.org/develop/dapps/asset-processing/jettons#retrieving-jetton-wallet-addresses-for-a-given-user)\ + [How to calculate user's Jetton wallet address (offline)?](https://docs.ton.org/v3/guidelines/dapps/cookbook#how-to-calculate-users-jetton-wallet-address-offline) + +::: + +## Liquidity Provisioning + +To provide liquidity to a particular DeDust pool, you must provide both assets. The pool will then issue special _LP tokens_ to the depositor's address. + +Read more about liquidity provisioning in the [DeDust documentation](https://docs.dedust.io/docs/liquidity-provisioning). + +```tact +import "@stdlib/deploy"; + +/// https://docs.dedust.io/reference/tlb-schemes#message-deposit_liquidity-1 +message(0x40e108d6) JettonDepositLiquidity { + // Pool type: 0 for volatile, 1 for stable + // Volatile pool is based on the "Constant Product" formula + // Stable-swap pool is optimized for assets of near-equal value, + // e.g. USDT/USDC, TON/stTON, etc. + poolType: Int as uint1; + + // Provided assets + asset0: Asset; + asset1: Asset; + + // Minimal amount of LP tokens to be received + // If there's less liquidity provided, the provisioning will be rejected + // Defaults to 0, makes this value ignored + minimalLpAmount: Int as coins = 0; + + // Target amount of the first asset + targetBalances0: Int as coins; + + // Target amount of the second asset + targetBalances1: Int as coins; + + // Custom payload attached to the transaction if the provisioning is successful + // Defaults to `null`, which means no payload + fulfillPayload: Cell? = null; + + // Custom payload attached to the transaction if the provisioning is rejected + // Defaults to `null`, which means no payload + rejectPayload: Cell? = null; +} + +/// https://docs.dedust.io/reference/tlb-schemes#message-deposit_liquidity +message(0xd55e4686) NativeDepositLiquidity { + // Unique identifier used to trace transactions across multiple contracts + // Defaults to 0, which means we don't mark messages to trace their chains + queryId: Int as uint64 = 0; + + // Toncoin amount for the deposit + amount: Int as coins; + + // Inlined fields of JettonDepositLiquidity Message without the opcode prefix + poolType: Int as uint1; + asset0: Asset; + asset1: Asset; + minimalLpAmount: Int as coins = 0; + targetBalances0: Int as coins; + targetBalances1: Int as coins; + fulfillPayload: Cell? = null; + rejectPayload: Cell? = null; +} + +/// https://docs.dedust.io/reference/tlb-schemes#asset +struct Asset { + // Specify 0 for native (TON) and omit all following fields + // Specify 1 for Jetton and then you must set non-null values for the following fields + type: Int as uint4; + + workchain: Int as uint8 = 0; // Both this zeroes will be removed during .build() function. Only type will remain. + address: Int as uint256 = 0; +} + +const PoolTypeVolatile: Int = 0; +const PoolTypeStable: Int = 1; + +const AssetTypeNative: Int = 0b0000; +const AssetTypeJetton: Int = 0b0001; + +const JettonProvideLpGas: Int = ton("0.5"); +const JettonProvideLpGasFwd: Int = ton("0.4"); +const TonProvideLpGas: Int = ton("0.15"); + +// This example directly uses the provided `myJettonWalletAddress` +// In real-world scenarios, it's more reliable to calculate this address on-chain or save it during initialization to prevent any issues +fun provideLiquidity(myJettonWalletAddress: Address) { + let jettonMasterRaw = parseStdAddress( + address("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs") + .asSlice() + ); + + // Step 1. Prepare input + let jettonAmount = ton("1"); + let tonAmount = ton("1"); + + let asset0 = Asset{ + type: AssetTypeNative, + }; + let asset1 = Asset{ + type: AssetTypeJetton, + workchain: jettonMasterRaw.workchain, + address: jettonMasterRaw.address, + }; + + // Step 2. Deposit Jetton to Vault + let jettonDepositBody = JettonDepositLiquidity{ + poolType: PoolTypeVolatile, + asset0, + asset1, + targetBalances0: tonAmount, + targetBalances1: jettonAmount, + }.build(); // notice the .build() and not .toCell(), + // since we want some custom serialization logic! + + send(SendParameters{ + to: myJettonWalletAddress, + value: JettonProvideLpGas, + body: JettonTransfer{ + queryId: 42, + amount: jettonAmount, + // Jetton Vault + destination: address("EQAYqo4u7VF0fa4DPAebk4g9lBytj2VFny7pzXR0trjtXQaO"), + responseDestination: myAddress(), + forwardTonAmount: JettonProvideLpGasFwd, + forwardPayload: jettonDepositBody, + }.toCell() + }); + + // Step 3. Deposit TON to Vault + let nativeDepositBody = NativeDepositLiquidity{ + queryId: 42, + amount: tonAmount, + poolType: PoolTypeVolatile, + asset0, + asset1, + targetBalances0: tonAmount, + targetBalances1: jettonAmount, + }.build(); // notice the .build() and not .toCell(), + // since we want some custom serialization logic! + + send(SendParameters{ + to: address("EQDa4VOnTYlLvDJ0gZjNYm5PXfSmmtL6Vs6A_CZEtXCNICq_"), + value: tonAmount + TonProvideLpGas, + body: nativeDepositBody, + }); +} + +// +// Helper extension functions to build respective Structs and Messages +// + +extends fun build(self: Asset): Cell { + let assetBuilder = beginCell() + .storeUint(self.type, 4); + + if (self.type == AssetTypeNative) { + return assetBuilder.endCell(); + } + + if (self.type == AssetTypeJetton) { + return assetBuilder + .storeUint(self.workchain, 8) + .storeUint(self.address, 256) + .endCell(); + } + + // Unknown asset type + return beginCell().endCell(); +} + +extends fun build(self: JettonDepositLiquidity): Cell { + return beginCell() + .storeUint(0x40e108d6, 32) + .storeUint(self.poolType, 1) + .storeSlice(self.asset0.build().asSlice()) + .storeSlice(self.asset1.build().asSlice()) + .storeCoins(self.minimalLpAmount) + .storeCoins(self.targetBalances0) + .storeCoins(self.targetBalances1) + .storeMaybeRef(self.fulfillPayload) + .storeMaybeRef(self.rejectPayload) + .endCell(); +} + +extends fun build(self: NativeDepositLiquidity): Cell { + return beginCell() + .storeUint(0xd55e4686, 32) + .storeUint(self.queryId, 64) + .storeCoins(self.amount) + .storeUint(self.poolType, 1) + .storeSlice(self.asset0.build().asSlice()) + .storeSlice(self.asset1.build().asSlice()) + .storeRef( + beginCell() + .storeCoins(self.minimalLpAmount) + .storeCoins(self.targetBalances0) + .storeCoins(self.targetBalances1) + .endCell() + ) + .storeMaybeRef(self.fulfillPayload) + .storeMaybeRef(self.rejectPayload) + .endCell(); +} + +// +// Messages from the Jetton standard +// + +message(0xf8a7ea5) JettonTransfer { + queryId: Int as uint64; + amount: Int as coins; + destination: Address; + responseDestination: Address?; + customPayload: Cell? = null; + forwardTonAmount: Int as coins; + forwardPayload: Cell?; // slightly adjusted +} +``` + +### Withdraw liquidity + +To withdraw liquidity, burning LP tokens is required. You can refer to examples of Jetton burning in the [respective section of Jettons Cookbook page](/cookbook/jettons#burning-jetton). However, more Toncoins should be added than for the normal burn, since adding too few may result in LP tokens being burned, but no (or only partial) liquidity being sent from the pool. Therefore, consider attaching at least $0.5$ Toncoin — excess amount will be returned. + +:::tip[Hey there!] + +Didn't find your favorite example of DeDust interaction? Have cool implementations in mind? [Contributions are welcome!](https://github.com/tact-lang/tact/issues) :::