diff --git a/ride/lp_stable.ride b/ride/lp_stable.ride index 88b37557e..d981047f0 100644 --- a/ride/lp_stable.ride +++ b/ride/lp_stable.ride @@ -989,118 +989,105 @@ func skipOrderValidation() = { fca.getBoolean(keySkipOrderValidation(this.toString())).valueOrElse(false) } -@Callable(i) -func calculateAmountOutForSwapREADONLY(cleanAmountIn: Int, isReverse: Boolean, feePoolAmount: Int) = { - let (assetOut, poolAmountInBalance) = if (isReverse == false) then { - let assetOut = this.strf(pa()) - let poolAmountInBalance = getAccBalance(this.strf(aa())).toBigInt() + cleanAmountIn.toBigInt() - (assetOut, poolAmountInBalance) - } else { - let assetOut = this.strf(aa()) - let poolAmountInBalance = getAccBalance(this.strf(pa())).toBigInt() + cleanAmountIn.toBigInt() - (assetOut, poolAmountInBalance) - } +let keyFactoryAddress = ["%s", "factoryContract"].makeString(SEP) +let factoryAddress = this.getStringValue(keyFactoryAddress).addressFromStringValue() - let poolConfig = gpc() - let amId = poolConfig[idxAmAsId] - let prId = poolConfig[idxPrAsId] - let xp = [amId.getAccBalance().toBigInt(), prId.getAccBalance().toBigInt()] - - let D = xp.getD() - let y = getY(isReverse, D, cleanAmountIn.toBigInt()) +let keyImplAddress = ["%s", "lpStableImpl"].makeString(SEP) +let implAddress = factoryAddress.getStringValue(keyImplAddress).addressFromStringValue() - let dy = getAccBalance(assetOut).toBigInt() - y - toBigInt(1) # -1 just in case there were some rounding errors +func mustAddress(i: Invocation, address: Address) = { + i.caller == address || throw() +} - let totalGetRaw = [0, dy.toInt()].max() +func mustImpl(i: Invocation) = { + mustAddress(i, implAddress) +} - let newXp = if (isReverse == false) then { - [amId.getAccBalance().toBigInt() + cleanAmountIn.toBigInt() + feePoolAmount.toBigInt(), prId.getAccBalance().toBigInt() - dy] - } else { - [amId.getAccBalance().toBigInt() - dy, prId.getAccBalance().toBigInt() + cleanAmountIn.toBigInt() + feePoolAmount.toBigInt()] - } - let newD = newXp.getD() - strict checkD = newD >= D || makeString(["new D is fewer error", D.toString(), newD.toString()], "__").throw() - - (nil, totalGetRaw) +@Callable(i) +func stringEntry(key: String, val: String) = + if (i.mustImpl()) then ([StringEntry(key, val)], key) else ([], unit) + +@Callable(i) +func integerEntry(key: String, val: Int) = + if (i.mustImpl()) then ([IntegerEntry(key, val)], key) else ([], unit) + +@Callable(i) +func transferAsset(recipientBytes: ByteVector, amount: Int, assetIdString: String) = { + let assetId = if (assetIdString == wavesString) then unit else assetIdString.fromBase58String() + if (i.mustImpl()) then ([ScriptTransfer(Address(recipientBytes), amount, assetId)], amount) else ([], unit) } @Callable(i) -func calculateAmountOutForSwapAndSendTokens(cleanAmountIn: Int, isReverse: Boolean, amountOutMin: Int, addressTo: String, feePoolAmount: Int) = { - let swapContact = invoke( - fca, - "getSwapContractREADONLY", - [], - [] - ).exactAs[String] +func callInvoke(recipientBytes: ByteVector, funcName: String, paymentAssetIdString: String, paymentAmount: Int) = { + let assetId = if (paymentAssetIdString == wavesString) then unit else paymentAssetIdString.fromBase58String() + let dappAddress = Address(recipientBytes) + if (i.mustImpl()) then ([], dappAddress.invoke(funcName, [], [AttachedPayment(assetId, paymentAmount)])) else ([], unit) +} - let isPoolSwapDisabled = fca.invoke( - "isPoolSwapDisabledREADONLY", - [this.toString()], - [] - ).exactAs[Boolean] - let isSwapDisabled = !isAddressWhitelisted(i.caller) && (igs() || cfgPoolStatus == PoolShutdown || isPoolSwapDisabled) +@Callable(i) +func callEmit(dappAddress: ByteVector, emitLpAmt: Int) = { + if (i.mustImpl()) then ([], Address(dappAddress).invoke("emit", [emitLpAmt], [])) else ([], unit) +} - strict checks = [ - !isSwapDisabled || i.isManager() || "swap operation is blocked by admin".throwErr(), - i.payments[0].value().amount >= cleanAmountIn || "Wrong amount".throwErr(), - i.caller == addressFromStringValue(swapContact) || "Permission denied".throwErr() - ] - let pmt = i.payments[0].value() - let assetIn = pmt.assetId.assetIdToString() +@Callable(i) +func callPut(dappAddress: ByteVector, assetId: String, amount: Int) = { + if (i.mustImpl()) then ([], Address(dappAddress).invoke("put", [], [AttachedPayment(assetId.parseAssetId(), amount)])) else ([], unit) +} - let (assetOut, poolAmountInBalance) = if (isReverse == false) then { - let assetOut = this.strf(pa()) - let poolAmountInBalance = getAccBalance(assetIn) - i.payments[0].value().amount - (assetOut, poolAmountInBalance) - } else { - let assetOut = this.strf(aa()) - let poolAmountInBalance = getAccBalance(assetIn) - i.payments[0].value().amount - (assetOut, poolAmountInBalance) - } +@Callable(i) +func callStake(dappAddress: ByteVector, assetId: String, amount: Int) = { + if (i.mustImpl()) then ([], Address(dappAddress).invoke("stake", [], [AttachedPayment(assetId.parseAssetId(), amount)])) else ([], unit) +} - let poolConfig = gpc() - let amId = poolConfig[idxAmAsId] - let prId = poolConfig[idxPrAsId] - let xp = if (isReverse == false) then { - [amId.getAccBalance().toBigInt() - i.payments[0].value().amount.toBigInt(), prId.getAccBalance().toBigInt()] - } else { - [amId.getAccBalance().toBigInt(), prId.getAccBalance().toBigInt() - i.payments[0].value().amount.toBigInt()] - } - let D = xp.getD() - let y = getY(isReverse, D, toBigInt(0)) +@Callable(i) +func callStakeFor(dappAddress: ByteVector, stakeAddress: String,assetId: String, amount: Int) = { + if (i.mustImpl()) then ([], Address(dappAddress).invoke("stakeFor", [stakeAddress], [AttachedPayment(assetId.parseAssetId(), amount)])) else ([], unit) +} - let dy = getAccBalance(assetOut).toBigInt() - y - toBigInt(1) # -1 just in case there were some rounding errors +@Callable(i) +func callBurn(dappAddress: ByteVector, assetId: String, amount: Int) = { + if (i.mustImpl()) then ([], Address(dappAddress).invoke("burn", [amount], [AttachedPayment(assetId.parseAssetId(), amount)])) else ([], unit) +} - let totalGetRaw = [0, dy.toInt()].max() +@Callable(i) +func callUnstake(dappAddress: ByteVector, assetId: String, amount: Int) = { + if (i.mustImpl()) then ([], Address(dappAddress).invoke("unstake", [assetId, amount], [])) else ([], unit) +} - strict checkMin = amountOutMin <= totalGetRaw || "Exchange result is fewer coins than expected".throw() +@Callable(i) +func callUnstakeINTERNAL(dappAddress: ByteVector, assetId: String, amount: Int, userAddress: ByteVector, recipientAddress: ByteVector) = { + if (i.mustImpl()) then ([], Address(dappAddress).invoke("unstakeINTERNAL", [assetId, amount, userAddress, recipientAddress], [])) else ([], unit) +} - let newXp = if (isReverse == false) then { - [amId.getAccBalance().toBigInt() + feePoolAmount.toBigInt(), prId.getAccBalance().toBigInt() - dy] - } else { - [amId.getAccBalance().toBigInt() - dy, prId.getAccBalance().toBigInt() + feePoolAmount.toBigInt()] - } - let newD = newXp.getD() - strict checkD = newD >= D || "new D is fewer error".throw() - let amountAssetBalanceDelta = if (isReverse) then { - -totalGetRaw - } else { - feePoolAmount - } - let priceAssetBalanceDelta = if (isReverse) then { - feePoolAmount - } else { - -totalGetRaw - } - strict refreshDLpActions = refreshDLpInternal(amountAssetBalanceDelta, priceAssetBalanceDelta, 0)._1 +@Callable(i) +func calculateAmountOutForSwapREADONLY(cleanAmountIn: Int, isReverse: Boolean, feePoolAmount: Int) = { + let totalGetRaw = implAddress.reentrantInvoke( + "calculateAmountOutForSwapREADONLY", + [cleanAmountIn, isReverse, feePoolAmount], + [] + ) - ( + (nil, totalGetRaw) +} + +@Callable(i) +func calculateAmountOutForSwapAndSendTokens(cleanAmountIn: Int, isReverse: Boolean, amountOutMin: Int, addressTo: String, feePoolAmount: Int) = { + strict totalGetRaw = implAddress.reentrantInvoke( + "calculateAmountOutForSwapAndSendTokens", [ - ScriptTransfer(addressTo.addressFromStringValue(), totalGetRaw, assetOut.parseAssetId()) + cleanAmountIn, + isReverse, + amountOutMin, + addressTo, + feePoolAmount, + i.payments[0].assetId.assetIdToString(), + i.payments[0].amount ], - totalGetRaw + [] ) + + ([], totalGetRaw) } @Callable(i) @@ -1122,196 +1109,68 @@ func constructor(fc: String) = { # 2. slipage is not bigger that current tokens ratio # arguments: # slippageTolerance max allowed slippage -# shouldAutoStake perform LP staking immediatelly in case true otherwise transfer LP to user) +# shouldAutoStake perform LP staking immediately in case true otherwise transfer LP to user) # attach: # attached should be two valid tokens from the available pools. # return: # transfer LP tokens based on deposit share @Callable(i) func put(slip: Int, autoStake: Boolean) = { - let factCfg = gfc() - let stakingCntr = addressFromString(factCfg[idxFactStakCntr]).valueOrErrorMessage("Wr st addr") - let slipCntr = addressFromString(factCfg[idxFactSlippCntr]).valueOrErrorMessage("Wr sl addr") - if(slip < 0) then throw("Wrong slippage") else - if (i.payments.size() != 2) then throw("2 pmnts expd") else - - let amAssetPmt = i.payments[0].value().amount.toBigInt() - let prAssetPmt = i.payments[1].value().amount.toBigInt() - - strict amountAssetBalance = cfgAmountAssetId.assetIdToString().getAccBalance().toBigInt() - amAssetPmt - strict priceAssetBalance = cfgPriceAssetId.assetIdToString().getAccBalance().toBigInt() - prAssetPmt - strict lpAssetEmission = assetInfo(cfgLpAssetId).value().quantity.toBigInt() - strict currentDLp = calcCurrentDLp(amAssetPmt, prAssetPmt, 0.toBigInt()) - # estPut - let e = cp(i.caller.toString(), - i.transactionId.toBase58String(), - AttachedPayment(i.payments[0].value().assetId, i.payments[0].value().amount), - i.payments[1], - slip, - true, - false, - true, - 0, - "") - - let emitLpAmt = e._2 - let lpAssetId = e._7 - let state = e._9 - let amDiff = e._10 - let prDiff = e._11 - let amId = e._12 - let prId = e._13 - - # emit lp on factory - strict r = fca.invoke("emit", [emitLpAmt], []) - # if the lp instance address is in the legacy list then the legacy factory address will be returned from the factory - strict el = match (r) { - case legacy: Address => legacy.invoke("emit", [emitLpAmt], []) - case _ => unit - } - strict sa = if(amDiff > 0) then slipCntr.invoke("put",[],[AttachedPayment(amId, amDiff)]) else [] - strict sp = if(prDiff > 0) then slipCntr.invoke("put",[],[AttachedPayment(prId, prDiff)]) else [] - - let lpTrnsfr = - if(autoStake) then strict ss = stakingCntr.invoke("stake",[],[AttachedPayment(lpAssetId, emitLpAmt)]); [] - else [ScriptTransfer(i.caller, emitLpAmt, lpAssetId)] - - let (refreshDLpActions, updatedDLp) = refreshDLpInternal(0, 0, 0) - - # strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) - strict check = updatedDLp >= currentDLp || [ - "updated DLp lower than current DLp", - amountAssetBalance.toString(), - priceAssetBalance.toString(), - lpAssetEmission.toString(), - currentDLp.toString(), - updatedDLp.toString(), - amDiff.toString(), - prDiff.toString() - ].makeString(" ").throwErr() - - strict lpAssetEmissionAfter = assetInfo(cfgLpAssetId).value().quantity - - state - ++ lpTrnsfr - ++ refreshDLpActions + strict putInvoke = implAddress.reentrantInvoke( + "put", + [ + slip, + autoStake, + i.payments[0].assetId.assetIdToString(), + i.payments[0].amount, + i.payments[1].assetId.assetIdToString(), + i.payments[1].amount + ], + [] + ) + + ([], nil) } @Callable(i) func putOneTknV2(minOutAmount: Int, autoStake: Boolean) = { - let isPoolOneTokenOperationsDisabled = fca.invoke( - "isPoolOneTokenOperationsDisabledREADONLY", - [this.toString()], - [] - ).exactAs[Boolean] - let isPutDisabled = !isAddressWhitelisted(i.caller) && (igs() || cfgPoolStatus == PoolPutDis || cfgPoolStatus == PoolShutdown || isPoolOneTokenOperationsDisabled) - strict checks = [ - !isPutDisabled || i.isManager() || "put operation is blocked by admin".throwErr(), i.payments.size() == 1 || "exactly 1 payment are expected".throwErr() ] - let amId = cfgAmountAssetId.value().toBase58String() - let prId = cfgPriceAssetId.value().toBase58String() - let lpId = cfgLpAssetId - let amDecimals = cfgAmountAssetDecimals - let prDecimals = cfgPriceAssetDecimals - - let userAddress = if (i.caller == this) then i.originCaller else i.caller - - let pmt = i.payments[0].value() - let pmtAssetId = pmt.assetId.value().toBase58String() - let pmtAmt = pmt.amount - - strict currentDLp = if (pmt.assetId == cfgAmountAssetId) then { - calcCurrentDLp(pmtAmt.toBigInt(), 0.toBigInt(), 0.toBigInt()) - } else { - calcCurrentDLp(0.toBigInt(), pmtAmt.toBigInt(), 0.toBigInt()) - } - - strict (estimLP, state, feeAmount) = calcPutOneTkn( - pmtAmt, - pmtAssetId, - userAddress.toString(), - i.transactionId.toBase58String(), - true + strict emitLpAmt = implAddress.reentrantInvoke( + "putOneTknV2", + [ + minOutAmount, + autoStake, + i.payments[0].assetId.assetIdToString(), + i.payments[0].amount + ], + [] ) - let emitLpAmt = if (minOutAmount > 0 && estimLP < minOutAmount) then { - ["amount to receive is less than ", minOutAmount.toString()].makeString("").throwErr() - } else estimLP - - # emit lp on factory - strict e = fca.invoke("emit", [emitLpAmt], []) - # if the lp instance address is in the legacy list then the legacy factory address will be returned from the factory - strict el = match (e) { - case legacy: Address => legacy.invoke("emit", [emitLpAmt], []) - case _ => unit - } - - let lpTrnsfr = - if (autoStake) then { - strict ss = stakingContract.invoke( - "stakeFor", - [i.caller.toString()], - [AttachedPayment(lpId, emitLpAmt)] - ) - - [] - } else [ScriptTransfer(i.caller, emitLpAmt, lpId)] - - let sendFeeToMatcher = if (feeAmount > 0) then [ScriptTransfer(feeCollectorAddress, feeAmount, pmtAssetId.fromBase58String())] else [] - - let (amountAssetBalanceDelta, priceAssetBalanceDelta) = { - if (this == feeCollectorAddress) then (0, 0) else { - # full payment asset id validation is in calcPutOneTkn - let paymentInAmountAsset = if (pmt.assetId == cfgAmountAssetId) then true else false - if (paymentInAmountAsset) then (-feeAmount, 0) else (0, -feeAmount) - } - } - let (refreshDLpActions, updatedDLp) = refreshDLpInternal(amountAssetBalanceDelta, priceAssetBalanceDelta, 0) - - strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) - - ( - state - ++ lpTrnsfr - ++ sendFeeToMatcher - ++ refreshDLpActions, - emitLpAmt - ) + ([], emitLpAmt) } # Put without LP emission @Callable(i) func putForFree(maxSlpg: Int) = { - if(maxSlpg < 0) then throw("Wrong slpg") else - if (i.payments.size() != 2) then throw("2 pmnts expd") else - let estPut = cp(i.caller.toString(), - i.transactionId.toBase58String(), - AttachedPayment(i.payments[0].value().assetId, i.payments[0].value().amount), - i.payments[1], - maxSlpg, - false, - false, - true, - 0, - "") - - let state = estPut._9 + strict check = i.payments.size() == 2 || throw("2 pmnts expd") - let amAssetPmt = i.payments[0].value().amount.toBigInt() - let prAssetPmt = i.payments[1].value().amount.toBigInt() - - strict currentDLp = calcCurrentDLp(amAssetPmt, prAssetPmt, 0.toBigInt()) - - let (refreshDLpActions, updatedDLp) = refreshDLpInternal(0, 0, 0) - - strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + strict putInvoke = implAddress.reentrantInvoke( + "putForFree", + [ + maxSlpg, + i.payments[0].assetId.assetIdToString(), + i.payments[0].amount, + i.payments[1].assetId.assetIdToString(), + i.payments[1].amount + ], + [] + ) - state ++ refreshDLpActions + ([], nil) } - # Called by: LP # # purpose: @@ -1325,80 +1184,48 @@ func putForFree(maxSlpg: Int) = { # transfer to user his share of pool tokens base on passed lp token amount @Callable(i) func get() = { - # amountAssetBalance - # strict aab = cfgAmountAssetId.assetIdToString().getAccBalance().toBigInt() - # # priceAssetBalance - # strict pab = cfgPriceAssetId.assetIdToString().getAccBalance().toBigInt() - # # lpAssetEmission - # strict lae = assetInfo(cfgLpAssetId).value().quantity.toBigInt() - # # lpAssetEmissionAfter - # strict laea = lae - i.payments[0].value().amount.toBigInt() - strict currentDLp = calcCurrentDLp(0.toBigInt(), 0.toBigInt(), 0.toBigInt()) - - let r = cg(i) - let outAmtAmt = r._1 - let outPrAmt = r._2 - let pmtAmt = r._3 - let pmtAssetId = r._4 - let state = r._5 - strict b = invoke(fca, - "burn", - [pmtAmt], - [AttachedPayment(pmtAssetId, pmtAmt)]) - - let (refreshDLpActions, updatedDLp) = refreshDLpInternal(-outAmtAmt, -outPrAmt, 0) - - strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + strict getInvoke = implAddress.reentrantInvoke( + "get", + [ + i.payments[0].assetId.assetIdToString(), + i.payments[0].amount + ], + [] + ) - state ++ refreshDLpActions + ([], nil) } @Callable(i) func getOneTknV2(outAssetId: String, minOutAmount: Int) = { - let isPoolOneTokenOperationsDisabled = fca.invoke( - "isPoolOneTokenOperationsDisabledREADONLY", - [this.toString()], - [] - ).exactAs[Boolean] - let isGetDisabled = !isAddressWhitelisted(i.caller) && (igs() || cfgPoolStatus == PoolShutdown || isPoolOneTokenOperationsDisabled) - strict checks = [ - !isGetDisabled || i.isManager() || "get operation is blocked by admin".throwErr(), i.payments.size() == 1 || "exactly 1 payment are expected".throwErr() ] - let (state, totalAmount) = getOneTknV2Internal( - outAssetId, - minOutAmount, - i.payments, - i.caller, - i.originCaller, - i.transactionId + strict totalAmount = implAddress.reentrantInvoke( + "getOneTknV2", + [ + outAssetId, + minOutAmount, + i.payments[0].assetId.assetIdToString(), + i.payments[0].amount + ], + [] ) - (state, totalAmount) + ([], totalAmount) } @Callable(i) func refreshDLp() = { - let lastRefreshedBlockHeight = keyDLpRefreshedHeight.getInteger().valueOrElse(0) - strict checkLastRefreshedBlockHeight = if (height - lastRefreshedBlockHeight >= dLpRefreshDelay) then unit else { - [ - dLpRefreshDelay.toString(), - " blocks have not passed since the previous call" - ].makeString("").throwErr() - } - - let dLp = this.getString(keyDLp) - .valueOrElse("0") - .parseBigInt() - .valueOrErrorMessage("invalid dLp".fmtErr()) - - let (dLpUpdateActions, updatedDLp) = refreshDLpInternal(0, 0, 0) - let actions = if (dLp != updatedDLp) then dLpUpdateActions else "nothing to refresh".throwErr() + strict updatedDLpString = implAddress.reentrantInvoke( + "refreshDLp", + [], + [] + ) - (actions, updatedDLp.toString()) + ([], updatedDLpString) } @Callable(i) @@ -1406,27 +1233,13 @@ func getOneTknV2READONLY( outAssetId: String, lpAssetAmount: Int ) = { - let amId = cfgAmountAssetId.value().toBase58String() - let prId = cfgPriceAssetId.value().toBase58String() - let lpId = cfgLpAssetId.value().toBase58String() - let xp = [amId.getAccBalance().toBigInt(), prId.getAccBalance().toBigInt()] - # let xp = [prId.getAccBalance().toBigInt(), amId.getAccBalance().toBigInt()] - let lpEmission = lpId.fromBase58String().assetInfo().valueOrErrorMessage("invalid lp asset").quantity.toBigInt() - let D0 = xp.getD() - let D1 = D0 - fraction(lpAssetAmount.toBigInt(), D0, lpEmission) - let index = if (outAssetId == amId) then { - 0 - } else if (outAssetId == prId) then { - 1 - } else { - "invalid out asset id".throw() - } - let newY = xp.getYD(index, D1) - let dy = xp[index] - newY - let totalGetRaw = [0, { dy - big1 }.toInt()].max() - let (totalGet, feeAmount) = totalGetRaw.takeFee(outFee) + strict outData = implAddress.reentrantInvoke( + "getOneTknV2READONLY", + [this.toString(), outAssetId, lpAssetAmount], + [] + ) - (nil, (totalGet, feeAmount)) + ([], outData) } @Callable(i) @@ -1434,176 +1247,89 @@ func getOneTknV2WithBonusREADONLY( outAssetId: String, lpAssetAmount: Int ) = { - let amId = cfgAmountAssetId.value().toBase58String() - let prId = cfgPriceAssetId.value().toBase58String() - let lpId = cfgLpAssetId.value().toBase58String() - - let amBalance = amId.getAccBalance() - let prBalance = prId.getAccBalance() - - let (totalGet, feeAmount) = this.invoke("getOneTknV2READONLY", [outAssetId, lpAssetAmount], []).exactAs[(Int, Int)] - - let r = ego( - "", - lpId, - lpAssetAmount, - this + strict outData = implAddress.reentrantInvoke( + "getOneTknV2WithBonusREADONLY", + [outAssetId, lpAssetAmount], + [] ) - let outAmAmt = r._1 - let outPrAmt = r._2 - let sumOfGetAssets = outAmAmt + outPrAmt - - let bonus = if (sumOfGetAssets == 0) then { - if (totalGet == 0) then 0 else "bonus calculation error".throw() - } else { - fraction((totalGet - sumOfGetAssets), scale8, sumOfGetAssets) - } - - ([], (totalGet, feeAmount, bonus)) + ([], outData) } @Callable(i) func getNoLess(noLessThenAmtAsset: Int, noLessThenPriceAsset: Int) = { - let r = cg(i) - let outAmAmt = r._1 - let outPrAmt = r._2 - let pmtAmt = r._3 - let pmtAssetId = r._4 - let state = r._5 - if (outAmAmt < noLessThenAmtAsset) then throw("Failed: " + outAmAmt.toString() + " < " + noLessThenAmtAsset.toString()) else - if (outPrAmt < noLessThenPriceAsset) then throw("Failed: " + outPrAmt.toString() + " < " + noLessThenPriceAsset.toString()) else - - strict currentDLp = calcCurrentDLp(0.toBigInt(), 0.toBigInt(), 0.toBigInt()) - - strict burnLPAssetOnFactory = invoke(fca, - "burn", - [pmtAmt], - [AttachedPayment(pmtAssetId, pmtAmt)]) - - let (refreshDLpActions, updatedDLp) = refreshDLpInternal(-outAmAmt, -outPrAmt, 0) + strict checks = [ + i.payments.size() == 1 || "exactly 1 payment are expected".throwErr() + ] - strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + strict inv = implAddress.reentrantInvoke( + "getNoLess", + [ + noLessThenAmtAsset, + noLessThenPriceAsset, + i.payments[0].assetId.assetIdToString(), + i.payments[0].amount + ], + [] + ) - state ++ refreshDLpActions + ([], nil) } # Unstake LP tokens and exit from pool @Callable(i) func unstakeAndGet(amount: Int) = { - strict checkPayments = if (i.payments.size() != 0) then throw("No pmnts expd") else true - - let factoryCfg = gfc() - - let lpAssetId = cfgLpAssetId - let staking = factoryCfg[idxFactStakCntr].addressFromString().valueOrErrorMessage("Wr st addr") - - strict currentDLp = calcCurrentDLp(0.toBigInt(), 0.toBigInt(), 0.toBigInt()) + strict checkPayments = (i.payments.size() == 0) || throw("No pmnts expd") - # negative amount will not pass - strict unstakeInv = staking.invoke("unstake", [lpAssetId.toBase58String(), amount], []) - - let r = ego(i.transactionId.toBase58String(), lpAssetId.toBase58String(), amount, i.caller) - let outAmAmt = r._1 - let outPrAmt = r._2 - let sts = r._9.parseIntValue() - let state = r._10 - - let isGetDisabled = !isAddressWhitelisted(i.caller) && (igs() || cfgPoolStatus == PoolShutdown) - strict v = if (isGetDisabled) then throw("Blocked: " + sts.toString()) else true - - strict burnA = invoke(fca, "burn", [amount], [AttachedPayment(lpAssetId, amount)]) - - let (refreshDLpActions, updatedDLp) = refreshDLpInternal(-outAmAmt, -outPrAmt, 0) - - strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + strict inv = implAddress.reentrantInvoke("unstakeAndGet", [amount], []) - state ++ refreshDLpActions + ([], nil) } @Callable(i) func unstakeAndGetNoLess(unstakeAmount: Int, noLessThenAmountAsset: Int, noLessThenPriceAsset: Int) = { - let isGetDisabled = !isAddressWhitelisted(i.caller) && (igs() || cfgPoolStatus == PoolShutdown) - - strict checks = [ - !isGetDisabled || "get operation is blocked by admin".throw(), - i.payments.size() == 0 || "no payments are expected".throw() - ] + strict checkPayments = (i.payments.size() == 0) || throw("no payments are expected") - strict currentDLp = calcCurrentDLp(0.toBigInt(), 0.toBigInt(), 0.toBigInt()) - - strict unstakeInv = stakingContract.invoke("unstake", [cfgLpAssetId.toBase58String(), unstakeAmount], []) - - let res = ego(i.transactionId.toBase58String(), cfgLpAssetId.toBase58String(), unstakeAmount, i.caller) - let outAmAmt = res._1 - let outPrAmt = res._2 - let state = res._10 - - strict checkAmounts = [ - outAmAmt >= noLessThenAmountAsset || ["amount asset amount to receive is less than ", noLessThenAmountAsset.toString()].makeString("").throw(), - outPrAmt >= noLessThenPriceAsset || ["price asset amount to receive is less than ", noLessThenPriceAsset.toString()].makeString("").throw() - ] - - strict burnLPAssetOnFactory = fca.invoke("burn", [unstakeAmount], [AttachedPayment(cfgLpAssetId, unstakeAmount)]) - - let (refreshDLpActions, updatedDLp) = refreshDLpInternal(-outAmAmt, -outPrAmt, 0) - - strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + strict inv = implAddress.reentrantInvoke( + "unstakeAndGetNoLess", + [ + unstakeAmount, + noLessThenAmountAsset, + noLessThenPriceAsset + ], + []) - state ++ refreshDLpActions + ([], nil) } @Callable(i) func unstakeAndGetOneTknV2(unstakeAmount: Int, outAssetId: String, minOutAmount: Int) = { - let isPoolOneTokenOperationsDisabled = fca.invoke( - "isPoolOneTokenOperationsDisabledREADONLY", - [this.toString()], - [] - ).exactAs[Boolean] - let isGetDisabled = !isAddressWhitelisted(i.caller) && (igs() || cfgPoolStatus == PoolShutdown || isPoolOneTokenOperationsDisabled) + strict checkPayments = (i.payments.size() == 0) || throw("no payments are expected") - strict checks = [ - !isGetDisabled || i.isManager() || "get operation is blocked by admin".throwErr(), - i.payments.size() == 0 || "no payments are expected".throwErr() - ] - - let factoryCfg = gfc() - - let lpAssetId = cfgLpAssetId - let staking = factoryCfg[idxFactStakCntr].addressFromString().valueOrErrorMessage("Wr st addr") - - let userAddress = i.caller - let lpAssetRecipientAddress = this - strict unstakeInv = staking.invoke("unstakeINTERNAL", [ - lpAssetId, - unstakeAmount, - userAddress.bytes, - lpAssetRecipientAddress.bytes - ], []) - let (state, totalAmount) = getOneTknV2Internal( - outAssetId, - minOutAmount, - [AttachedPayment(lpAssetId, unstakeAmount)], - i.caller, - i.originCaller, - i.transactionId - ) + strict totalAmount = implAddress.reentrantInvoke( + "unstakeAndGetOneTknV2", + [ + unstakeAmount, + outAssetId, + minOutAmount + ], + []) - (state, totalAmount) + ([], totalAmount) } @Callable(i) func putOneTknV2WithBonusREADONLY(paymentAmountRaw: Int, paymentAssetId: String) = { - let (lpAmount, state, feeAmount, bonus) = calcPutOneTkn(paymentAmountRaw, paymentAssetId, "", "", true) + let result = implAddress.reentrantInvoke("putOneTknV2WithBonusREADONLY", [paymentAmountRaw, paymentAssetId], []) - (nil, (lpAmount, feeAmount, bonus)) + (nil, result) } @Callable(i) func putOneTknV2WithoutTakeFeeREADONLY(paymentAmountRaw: Int, paymentAssetId: String) = { - let (lpAmount, state, feeAmount, bonus) = calcPutOneTkn(paymentAmountRaw, paymentAssetId, "", "", false) + let result = implAddress.reentrantInvoke("putOneTknV2WithoutTakeFeeREADONLY", [paymentAmountRaw, paymentAssetId], []) - (nil, (lpAmount, feeAmount, bonus)) + (nil, result) } # purpose: @@ -1631,55 +1357,44 @@ func activate(amtAsStr: String, prAsStr: String) = { # API wrappers @Callable(i) func getPoolConfigWrapperREADONLY() = { - ( - [], - gpc() - ) + let result = implAddress.reentrantInvoke("getPoolConfigWrapperREADONLY", [], []) + + (nil, result) } @Callable(i) func getAccBalanceWrapperREADONLY(assetId: String) = { - ( - [], - assetId.getAccBalance() - ) + let result = implAddress.reentrantInvoke("getAccBalanceWrapperREADONLY", [assetId], []) + + (nil, result) } @Callable(i) func calcPricesWrapperREADONLY(amAmt: Int, prAmt: Int, lpAmt: Int) = { - let pr = calcPrices(amAmt, prAmt, lpAmt) - ( - [], - [ - pr[0].toString(), - pr[1].toString(), - pr[2].toString() - ] - ) + let result = implAddress.reentrantInvoke("calcPricesWrapperREADONLY", [amAmt, prAmt, lpAmt], []) + + (nil, result) } @Callable(i) func fromX18WrapperREADONLY(val: String, resScaleMult: Int) = { - ( - [], - f1(val.parseBigIntValue(), resScaleMult) - ) + let result = implAddress.reentrantInvoke("fromX18WrapperREADONLY", [val, resScaleMult], []) + + (nil, result) } @Callable(i) func toX18WrapperREADONLY(origVal: Int, origScaleMult: Int) = { - ( - [], - t1(origVal, origScaleMult).toString() - ) + let result = implAddress.reentrantInvoke("toX18WrapperREADONLY", [origVal, origScaleMult], []) + + (nil, result) } @Callable(i) func calcPriceBigIntWrapperREADONLY(prAmtX18: String, amAmtX18: String) = { - ( - [], - cpbi(prAmtX18.parseBigIntValue(), amAmtX18.parseBigIntValue()).toString() - ) + let result = implAddress.reentrantInvoke("calcPriceBigIntWrapperREADONLY", [prAmtX18, amAmtX18], []) + + (nil, result) } @Callable(i) @@ -1694,10 +1409,10 @@ func estimatePutOperationWrapperREADONLY( isEval: Boolean, emitLp: Boolean ) = { - ( - [], - epo( - txId58, + let result = implAddress.reentrantInvoke( + "estimatePutOperationWrapperREADONLY", + [ + txId58, slippage, inAmAmt, inAmId, @@ -1705,27 +1420,28 @@ func estimatePutOperationWrapperREADONLY( inPrId, usrAddr, isEval, - emitLp, - true, - false, - 0, - "" - ) + emitLp + ], + [] ) + + (nil, result) } @Callable(i) func estimateGetOperationWrapperREADONLY(txId58: String, pmtAsId: String, pmtLpAmt: Int, usrAddr: String) = { - let r = ego( - txId58, - pmtAsId, - pmtLpAmt, - usrAddr.addressFromStringValue() - ) - ( - [], - (r._1, r._2, r._3, r._4, r._5, r._6, r._7, r._8.toString(), r._9, r._10) + let result = implAddress.reentrantInvoke( + "estimateGetOperationWrapperREADONLY", + [ + txId58, + pmtAsId, + pmtLpAmt, + usrAddr + ], + [] ) + + (nil, result) } @Callable(i) diff --git a/ride/lp_stable_impl.ride b/ride/lp_stable_impl.ride new file mode 100644 index 000000000..673397191 --- /dev/null +++ b/ride/lp_stable_impl.ride @@ -0,0 +1,2161 @@ +{-# STDLIB_VERSION 6 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +# TODO: +# +# add callable to remove existing pool +# add restriction on min deposit amount +# AMU: move "WAVES" into spec variable +# AMU: add safe cast method instead of copy/past: inAmAssetId.valueOrElse("WAVES".fromBase58String()).toBase58String() + +# Description: +# Contract represents single liquidity pool for specific asset pair, e.g. BTC-USDN +# +# Actors: +# 1. LP +# 2. Factory +# 3. Matcher +# +# Actor LP could do do the following: +# 1. Enter the pool +# 2. Exit the pool +# +# Factory LP could do do the following: +# 1. Activate the pool +# 2. Halt pool operations partially of completely +# +# Matcher LP could do do the following: +# 1. Perform exchange operations with pool assets +# +# New Pool deployment flow: +# 0. Factory contract has BLAKE2b-256 hash of the Pool contract. +# 1. Pool contract is deployed to the blockchain (factory address is injected into it) +# 2. Factory calls 'activate' callable and activate pool in case all prerequisites passed (asset pairs are not registered, contract hash matches actual) + +#----------------- +# GLOBAL VARIABLES +#----------------- +let scale8 = 100_000_000 +let scale8BigInt = 100_000_000.toBigInt() +let scale18 = 1_000_000_000_000_000_000.toBigInt() +let zeroBigInt = 0.toBigInt() +let big0 = 0.toBigInt() +let big1 = 1.toBigInt() +let big2 = 2.toBigInt() +let big3 = 3.toBigInt() +let big4 = 4.toBigInt() +let slippage4D = (scale8 - scale8 * 1 / scale8).toBigInt() # 9999999 or error of 0.0000001 +let wavesString = "WAVES" +let ampInitial = 50 + +let Amult = "100" +let Dconv = "1" # D convergence + +let SEP = "__" +let EMPTY = "" +let PoolActive = 1 # ACTIVE, pool without restrictions +let PoolPutDis = 2 # PUT DISABLED, pool with put operation disabled +let PoolMatcherDis = 3 # MATCHER DISABLED, pool with matcher operations disabled +let PoolShutdown = 4 # SHUTDOWN, pool operations halted +# data indexes from pool config stored in factory +let idxPoolAddress = 1 +let idxPoolSt = 2 +let idxLPAsId = 3 +let idxAmAsId = 4 +let idxPrAsId = 5 +let idxAmtAsDcm = 6 +let idxPriceAsDcm = 7 +let idxIAmtAsId = 8 +let idxIPriceAsId = 9 +# data indexes from factory config +let idxFactStakCntr = 1 +let idxFactoryRestCntr = 6 +let idxFactSlippCntr = 7 +let idxFactGwxRewCntr = 10 + +let feeDefault = fraction(10, scale8, 10_000) +#------------------------- +# WX COMMON LIBRARY +#------------------------- +func t1(origVal: Int, origScaleMult: Int) = fraction(origVal.toBigInt(), scale18, origScaleMult.toBigInt()) +func t1BigInt(origVal: BigInt, origScaleMult: BigInt) = fraction(origVal, scale18, origScaleMult) +func f1(val: BigInt, resultScaleMult: Int) = fraction(val, resultScaleMult.toBigInt(), scale18).toInt() +func fromX18Round( + val: BigInt, + resultScaleMult: Int, + round: Ceiling|Floor +) = fraction(val, resultScaleMult.toBigInt(), scale18, round).toInt() + +func t2(origVal: BigInt, origScaleMult: Int) = fraction(origVal, scale18, origScaleMult.toBigInt()) +func f2(val: BigInt, resultScaleMult: Int) = fraction(val, resultScaleMult.toBigInt(), scale18) + +# cast passed amount to specified 'resScale' scale value from 'curScale' scale value +func ts(amt: Int, resScale: Int, curScale: Int) = fraction(amt, resScale, curScale) + +func abs(val: BigInt) = if (val < zeroBigInt) then -val else val +func absBigInt(val: BigInt) = if (val < zeroBigInt) then -val else val + +#------------------------- +# KEYS ON CURRENT CONTRACT +#------------------------- +func fc() = {"%s__factoryContract"} +# keyManagerPublicKey +func keyManagerPublicKey() = "%s__managerPublicKey" +# keyManagerVaultAddress +func keyManagerVaultAddress() = "%s__managerVaultAddress" +func pl() = {"%s%s__price__last"} +func ph(h: Int, t: Int) = {makeString(["%s%s%d%d__price__history", h.toString(), t.toString()], SEP)} +# keyPutActionByUser +func pau(ua: String, txId: String) = "%s%s%s__P__" + ua + "__" + txId +# keyGetActionByUser +func gau(ua: String, txId: String) = "%s%s%s__G__" + ua + "__" + txId +func aa() = {"%s__amountAsset"} +func pa() = {"%s__priceAsset"} +# keyAmplificator +func amp() = {"%s__amp"} +func keyAmpHistory(heightBlocks: Int) = "%s%d__amp__" + heightBlocks.toString() +func keyChangeAmpLastCall() = "%s__changeAmpLastCall" + +let keyFee = "%s__fee" +let fee = this.getInteger(keyFee).valueOrElse(feeDefault) + +let keyDLp = ["%s", "dLp"].makeString(SEP) +let keyDLpRefreshedHeight = ["%s", "dLpRefreshedHeight"].makeString(SEP) +let keyDLpRefreshDelay = ["%s", "refreshDLpDelay"].makeString(SEP) +let dLpRefreshDelayDefault = 30 +func dLpRefreshDelay(poolAddress: Address) = poolAddress.getInteger(keyDLpRefreshDelay).valueOrElse(dLpRefreshDelayDefault) + +#------------------------ +# KEYS ON OTHER CONTRACTS +#------------------------ +# from factory +# keyFactoryConfig +func fcfg() = {"%s__factoryConfig"} +# keyMatcherPub +func mtpk() = "%s%s__matcher__publicKey" +# keyPoolConfig +func pc(iAmtAs: String, iPrAs: String) = {"%d%d%s__" + iAmtAs + "__" + iPrAs + "__config"} +# keyMappingsBaseAsset2internalId +func mba(bAStr: String) = {"%s%s%s__mappings__baseAsset2internalId__" + bAStr} +# keyAllPoolsShutdown +func aps() = {"%s__shutdown"} +func keyAllowedLpStableScriptHash() = "%s__allowedLpStableScriptHash" +func keyFeeCollectorAddress() = "%s__feeCollectorAddress" +func keySkipOrderValidation(poolAddress: String) = "%s%s__skipOrderValidation__" + poolAddress + +#------------------------ +# FAILURES +#------------------------ +func throwOrderError( + orderValid: Boolean, + orderValidInfo: String, + senderValid: Boolean, + matcherValid: Boolean +) = { + throw("order validation failed: orderValid=" + orderValid.toString() + " (" + orderValidInfo + ")" + " senderValid=" + senderValid.toString() + " matcherValid=" + matcherValid.toString()) +} + +#------------------------ +# GLOBAL FUNCTIONS +#------------------------ +func addressFromStringOrThis(addressString: String) = { + match(addressString.addressFromString()) { + case a:Address => a + case _ => this + } +} + +func getManagerVaultAddressOrThis() = { + let factoryAddress = match fc().getString() { + case fca:String => addressFromStringOrThis(fca) + case _ => this + } + + match factoryAddress.getString(keyManagerVaultAddress()) { + case s:String => addressFromStringOrThis(s) + case _=> this + } +} + +# getStringOrFail +func strf(addr: Address, key: String) = addr.getString(key).valueOrErrorMessage(makeString(["mandatory ", addr.toString(), ".", key, " not defined"], "")) +# getIntOrFail +func intf(addr: Address, key: String) = addr.getInteger(key).valueOrErrorMessage(makeString(["mandatory ", addr.toString(), ".", key, " not defined"], "")) + +func throwErr(msg: String) = ["lp_stable.ride:", msg].makeString(" ").throw() +func fmtErr(msg: String) = ["lp_stable.ride:", msg].makeString(" ") + +# factoryContract +let fca = addressFromStringValue(strf(this, fc())) + +let inFee = fca.invoke("getInFeeREADONLY", [this.toString()], []).exactAs[Int] +let outFee = fca.invoke("getOutFeeREADONLY", [this.toString()], []).exactAs[Int] + +func keyAddressWhitelisted(address: Address) = makeString(["%s%s", "whitelisted", address.toString()], SEP) + +func isAddressWhitelisted(address: Address) = { + fca.getBoolean(keyAddressWhitelisted(address)).valueOrElse(false) +} + +func A(poolAddress: Address) = strf(poolAddress, amp()) + +# isGlobalShutdown +# check that global shutdown is take place +func igs() = { + fca.getBoolean(aps()).valueOrElse(false) +} + +# getMatcherPubOrFail +func mp() = { + fca.strf(mtpk()).fromBase58String() +} + +let feeCollectorAddress = fca.strf(keyFeeCollectorAddress()).addressFromStringValue() + +# getPoolConfig +# function used to gather all pool data from factory +func gpc(address: Address) = { + let amtAs = strf(address, aa()) + let priceAs = strf(address, pa()) + let iPriceAs = intf(fca, mba(priceAs)) + let iAmtAs = intf(fca, mba(amtAs)) + strf(fca, pc(iAmtAs.toString(), iPriceAs.toString())).split(SEP) +} + +func parseAssetId(input: String) = { + if (input == wavesString) then unit else input.fromBase58String() +} + +func assetIdToString(input: ByteVector|Unit) = { + if (input == unit) then wavesString else input.value().toBase58String() +} + +func parsePoolConfig(poolConfig: List[String]) = { + ( + poolConfig[idxPoolAddress].addressFromStringValue(), + poolConfig[idxPoolSt].parseIntValue(), + poolConfig[idxLPAsId].fromBase58String(), + poolConfig[idxAmAsId].parseAssetId(), + poolConfig[idxPrAsId].parseAssetId(), + poolConfig[idxAmtAsDcm].parseIntValue(), + poolConfig[idxPriceAsDcm].parseIntValue() + ) +} + +# getFactoryConfig +func gfc() = { + strf(fca, fcfg()).split(SEP) +} + +let factoryConfig = gfc() +let stakingContract = factoryConfig[idxFactStakCntr].addressFromString().valueOrErrorMessage("Invalid staking contract address") +let slipageContract = factoryConfig[idxFactSlippCntr].addressFromString().valueOrErrorMessage("Invalid slipage contract address") +let gwxContract = factoryConfig[idxFactGwxRewCntr].addressFromString().valueOrErrorMessage("Invalid gwx contract address") +let restContract = factoryConfig[idxFactoryRestCntr].addressFromString().valueOrErrorMessage("Invalid gwx contract address") + +func dataPutActionInfo(inAmtAssetAmt: Int, inPriceAssetAmt: Int, outLpAmt: Int, price: Int, slipByUser: Int, slippageReal: Int, txHeight: Int, txTimestamp: Int, slipageAmAmt: Int, slipagePrAmt: Int) = { + makeString(["%d%d%d%d%d%d%d%d%d%d", inAmtAssetAmt.toString(), inPriceAssetAmt.toString(), outLpAmt.toString(), price.toString(), slipByUser.toString(), slippageReal.toString(), txHeight.toString(), txTimestamp.toString(), slipageAmAmt.toString(), slipagePrAmt.toString()], SEP) +} + +func dataGetActionInfo(outAmtAssetAmt: Int, outPriceAssetAmt: Int, inLpAmt: Int, price: Int, txHeight: Int, txTimestamp: Int) = { + makeString( ["%d%d%d%d%d%d", outAmtAssetAmt.toString(), outPriceAssetAmt.toString(), inLpAmt.toString(), price.toString(), txHeight.toString(), txTimestamp.toString()], SEP) +} + +func getAccBalance(poolAddress: Address, assetId: String) = { + if(assetId == "WAVES") then wavesBalance(poolAddress).available else assetBalance(poolAddress, fromBase58String(assetId)) +} + +# calcPriceBigInt +func cpbi(prAmtX18: BigInt, amAmtX18: BigInt) = { + fraction(prAmtX18, scale18, amAmtX18) +} + +func cpbir(prAmtX18: BigInt, amAmtX18: BigInt, round: Ceiling|Floor) = { + fraction(prAmtX18, scale18, amAmtX18, round) +} + +# validateAbsDiff +func vad(A1: BigInt, A2: BigInt, slippage: BigInt) = { + let diff = fraction(A1 - A2, scale8BigInt, A2) + let pass = (slippage - abs(diff)) > zeroBigInt + if (!pass) then throw("Big slpg: " + diff.toString()) else + (pass, min([A1, A2])) +} + +# validateD +func vd(D1: BigInt, D0: BigInt, slpg: BigInt) = { + let diff = fraction(D0, scale8BigInt, D1) + let fail = diff < slpg + if (fail || D1 < D0) then throw(D0.toString() + " " + D1.toString() + " " + diff.toString() + " " + slpg.toString()) else + fail +} + +# privateCalcPrice +# cast assets and calc price +func pcp(amAssetDcm: Int, prAssetDcm: Int, amAmt: Int, prAmt: Int) = { + let amtAsAmtX18 = amAmt.t1(amAssetDcm) + let prAsAmtX18 = prAmt.t1(prAssetDcm) + cpbi(prAsAmtX18, amtAsAmtX18) +} + +# used only in stats endpoint, so result values are in scale8 as required +func calcPrices(poolAddress: Address, amAmt: Int, prAmt: Int, lpAmt: Int) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let amtAsDcm = cfgAmountAssetDecimals + let prAsDcm = cfgPriceAssetDecimals + + let priceX18 = pcp(amtAsDcm, prAsDcm, amAmt, prAmt) + + let amAmtX18 = amAmt.t1(amtAsDcm) + let prAmtX18 = prAmt.t1(prAsDcm) + let lpAmtX18 = lpAmt.t1(scale8) + + let lpPrInAmAsX18 = cpbi(amAmtX18, lpAmtX18) + let lpPrInPrAsX18 = cpbi(prAmtX18, lpAmtX18) + + [priceX18, lpPrInAmAsX18, lpPrInPrAsX18] +} + +# public API which is used by backend +func calculatePrices(poolAddress: Address, amAmt: Int, prAmt: Int, lpAmt: Int) = { + + let p = calcPrices(poolAddress, amAmt, prAmt, lpAmt) + [p[0].f1(scale8), + p[1].f1(scale8), + p[2].f1(scale8)] +} + +func takeFee(amount: Int, fee: Int) = { + let feeAmount = if (fee == 0) then 0 else fraction(amount, fee, scale8) + (amount - feeAmount, feeAmount) +} + +func getD(poolAddress: Address, xp: List[BigInt]) = { + let xp0 = xp[0] + let xp1 = xp[1] + let s = xp0 + xp1 + if (s == big0) then big0 else { + let a = A(poolAddress).parseIntValue() + let ann = a * 2 + let p = fraction(xp0, xp1, big1) + let xp0_xp1_n_n = fraction(p, big4, big1) + let ann_s = fraction(ann.toBigInt(), s, big1) + let ann_1 = (ann - 1).toBigInt() + func calcDNext(d: BigInt) = { + let dd = fraction(d, d, big1) + let ddd = fraction(dd, d, big1) + let dp = fraction(ddd, big1, xp0_xp1_n_n) + fraction( + ann_s + fraction(dp, big2, big1), + d, + fraction(ann_1, d, big1) + fraction(big3, dp, big1) + ) + } + func calc(acc: (BigInt, Boolean), i: Int) = { + if (acc._2) then acc else { + let d = acc._1 + let dNext = d.calcDNext() + let dDiffRaw = dNext - d.value() + let dDiff = if (dDiffRaw < big0) then -dDiffRaw else dDiffRaw + if (dDiff <= big1) then { + (dNext, true) + } else { + (dNext, false) + } + } + } + let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + let (d, found) = FOLD<17>(arr, (s, false), calc) + if (found) then { + d + } else { + { "D calculation error, D = " + d.toString() }.throw() + } + } +} + +# estimateGetOperation +func ego(poolAddress: Address, readonly: Boolean, txId58: String, pmtAssetId: String, pmtLpAmt: Int, userAddress: Address) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let lpId = cfgLpAssetId + let amId = cfgAmountAssetId.value().toBase58String() + let prId = cfgPriceAssetId.value().toBase58String() + let amDcm = cfgAmountAssetDecimals + let prDcm = cfgPriceAssetDecimals + let sts = cfgPoolStatus.toString() + + let lpEmiss = assetInfo(lpId).valueOrErrorMessage("Wrong LP id").quantity + + # validation block + if (lpId.toBase58String() != pmtAssetId) then throw("Wrong pmt asset") else + + let amBalance = getAccBalance(poolAddress, amId) + let amBalanceX18 = amBalance.t1(amDcm) + + let prBalance = getAccBalance(poolAddress, prId) + let prBalanceX18 = prBalance.t1(prDcm) + + let curPriceX18 = cpbi(prBalanceX18, amBalanceX18) + let curPrice = curPriceX18.f1(scale8) + + let pmtLpAmtX18 = pmtLpAmt.t1(scale8) + let lpEmissX18 = lpEmiss.t1(scale8) + # calculations + let outAmAmtX18 = fraction(amBalanceX18, pmtLpAmtX18, lpEmissX18) + let outPrAmtX18 = fraction(prBalanceX18, pmtLpAmtX18, lpEmissX18) + + # cast amounts to asset decimals + let outAmAmt = outAmAmtX18.fromX18Round(amDcm, FLOOR) + let outPrAmt = outPrAmtX18.fromX18Round(prDcm, FLOOR) + + let state = if (txId58 == "") then [] else { + strict stateInvokes = if (!readonly) then [ + poolAddress.invoke( + "transferAsset", + [userAddress.bytes, outAmAmt, amId], + [] + ), + poolAddress.invoke( + "transferAsset", + [userAddress.bytes, outPrAmt, prId], + [] + ), + poolAddress.invoke( + "stringEntry", + [ + gau(userAddress.toString(), txId58), + dataGetActionInfo(outAmAmt, outPrAmt, pmtLpAmt, curPrice, height, lastBlock.timestamp) + ], + [] + ), + poolAddress.invoke( + "integerEntry", + [pl(), curPrice], + [] + ), + poolAddress.invoke( + "integerEntry", + [ph(height, lastBlock.timestamp), curPrice], + [] + ) + ] else [] + + [] + } + + ( outAmAmt, # 1 + outPrAmt, # 2 + amId, # 3 + prId, # 4 + amBalance, # 5 + prBalance, # 6 + lpEmiss, # 7 + curPriceX18, # 8 + sts, # 9 + state # 10 + ) +} + +# estimatePutOperation +func epo(poolAddress: Address, + readonly: Boolean, + txId58: String, + slippage: Int, + inAmAmt: Int, + inAmId: ByteVector|Unit, + inPrAmt: Int, + inPrId: ByteVector|Unit, + userAddress: String, + isEval: Boolean, + emitLp: Boolean, + isOneAsset: Boolean, + validateSlippage: Boolean, + pmtAmt: Int, + pmtId: String) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let lpId = cfgLpAssetId + let amIdStr = cfgAmountAssetId.value().toBase58String() + let prIdStr = cfgPriceAssetId.value().toBase58String() + let amtDcm = cfgAmountAssetDecimals + let priceDcm = cfgPriceAssetDecimals + let sts = cfgPoolStatus.toString() + + let lpEm = assetInfo(lpId).valueOrErrorMessage("Wr lp as").quantity + + strict checkAssets = [ + (inAmId == amIdStr.parseAssetId() && inPrId == prIdStr.parseAssetId()) || throwErr("Invalid amt or price asset passed.") + ] + + # get current balances from acc + let amBalance = if(isEval) then getAccBalance(poolAddress, amIdStr) else + if(isOneAsset && pmtId == amIdStr) then getAccBalance(poolAddress, amIdStr) - pmtAmt else + if(isOneAsset) then getAccBalance(poolAddress, amIdStr) else getAccBalance(poolAddress, amIdStr) - inAmAmt + let prBalance = if(isEval) then getAccBalance(poolAddress, prIdStr) else + if(isOneAsset && pmtId == prIdStr) then getAccBalance(poolAddress, prIdStr) - pmtAmt else + if(isOneAsset) then getAccBalance(poolAddress, prIdStr) else getAccBalance(poolAddress, prIdStr) - inPrAmt + + #if(true) then throw("lpEmission = "+ lpEmission.toString() + + # " amBalance = " + amBalance.toString() + + # " prBalance = " + prBalance.toString() + + # " getAccBalance(amIdStr) = " + getAccBalance(amIdStr).toString() + + # " etAccBalance(prIdStr) = " + getAccBalance(prIdStr).toString()) else + + # cast amounts to the lp decimals + let inAmAssetAmtX18 = inAmAmt.t1(amtDcm) + let inPrAssetAmtX18 = inPrAmt.t1(priceDcm) + + # calc user expected price + let userPriceX18 = cpbi(inPrAssetAmtX18, inAmAssetAmtX18) + + # calc pool price + let amBalanceX18 = amBalance.t1(amtDcm) + let prBalanceX18 = prBalance.t1(priceDcm) + + let D0 = getD(poolAddress, [amBalanceX18, prBalanceX18]) + + # case of the initial or first deposit + # result is a tuple containing the following: + # 1. lp amount that user got + # 2. amtAsset amount that goes to Pool liquidity + # 3. priceAsset amount that goes to Pool liquidity + # 4. pool price after PUT operation + let r = if(lpEm == 0) then { + let D1 = getD(poolAddress, [amBalanceX18 + inAmAssetAmtX18, prBalanceX18 + inPrAssetAmtX18]) + + strict checkD = D1 > D0 || "D1 should be greater than D0".throw() + let curPriceX18 = zeroBigInt + let slippageX18 = zeroBigInt + # calc initial deposit by geometric mean + # let lpAmtX18 = pow(inAmAssetAmtX18 * inPrAssetAmtX18, 0, 5.toBigInt(), 1, 0, DOWN) + let lpAmtX18 = D1 + ( + lpAmtX18.f1(scale8), + inAmAssetAmtX18.f1(amtDcm), + inPrAssetAmtX18.f1(priceDcm), + cpbi(prBalanceX18 + inPrAssetAmtX18, amBalanceX18 + inAmAssetAmtX18), + slippageX18 + ) + } else { + let curPriceX18 = cpbi(prBalanceX18, amBalanceX18) + let slippageRealX18 = fraction(abs(curPriceX18 - userPriceX18), scale18, curPriceX18) + let slippageX18 = slippage.t1(scale8) + # validate slippage + if (validateSlippage && curPriceX18 != zeroBigInt && slippageRealX18 > slippageX18) then throw("Price slippage " + slippageRealX18.toString() + " > " + slippageX18.toString()) else + + let lpEmissionX18 = lpEm.t1(scale8) + # calculate amount of price asset needed to deposit pool by current price and user's amountAsset amount + + let prViaAmX18 = fraction(inAmAssetAmtX18, cpbir(prBalanceX18, amBalanceX18, CEILING), scale18, CEILING) + let amViaPrX18 = fraction(inPrAssetAmtX18, scale18, cpbir(prBalanceX18, amBalanceX18, FLOOR), CEILING) + + # calculate amount and price assets to perform pool deposit in proportion to current pool price + let expectedAmts= if (prViaAmX18 > inPrAssetAmtX18) + then (amViaPrX18, inPrAssetAmtX18) + else (inAmAssetAmtX18, prViaAmX18) + + let expAmtAssetAmtX18 = expectedAmts._1 + let expPriceAssetAmtX18 = expectedAmts._2 + # calculate LP amount that user + # let lpAmtX18 = fraction(lpEmissionX18, expPriceAssetAmtX18, prBalanceX18, FLOOR) + let D1 = getD(poolAddress, [amBalanceX18 + expAmtAssetAmtX18, prBalanceX18 + expPriceAssetAmtX18]) + + strict checkD = D1 > D0 || "D1 should be greater than D0".throw() + let lpAmtX18 = fraction(lpEmissionX18, D1 - D0, D0) + ( + lpAmtX18.fromX18Round(scale8, FLOOR), + expAmtAssetAmtX18.fromX18Round(amtDcm, CEILING), + expPriceAssetAmtX18.fromX18Round(priceDcm, CEILING), + curPriceX18, + slippageX18 + ) + } + + let calcLpAmt = r._1 + let calcAmAssetPmt = r._2 + let calcPrAssetPmt = r._3 + let curPrice = r._4.f1(scale8) + let slippageCalc = r._5.f1(scale8) + + if(calcLpAmt <= 0) then throw("LP <= 0") else + + let emitLpAmt = if (!emitLp) then 0 else calcLpAmt + let amDiff = inAmAmt - calcAmAssetPmt + let prDiff = inPrAmt - calcPrAssetPmt + + let (writeAmAmt, writePrAmt) = if(isOneAsset && pmtId == amIdStr) + then (pmtAmt, 0) + else if(isOneAsset && pmtId == prIdStr) + then (0, pmtAmt) + else (calcAmAssetPmt, calcPrAssetPmt) + + let commonState = [] + + strict stateChanges = if (!readonly) then [ + poolAddress.invoke("integerEntry", [pl(), curPrice], []), + poolAddress.invoke("integerEntry", [ph(height, lastBlock.timestamp), curPrice], []), + poolAddress.invoke("stringEntry", [ + pau(userAddress, txId58), + dataPutActionInfo(writeAmAmt, writePrAmt, emitLpAmt, curPrice, slippage, slippageCalc, height, lastBlock.timestamp, amDiff, prDiff) + ], []) + ] else [] + + ( + calcLpAmt, # 1. + emitLpAmt, # 2. + curPrice, # 3. + amBalance, # 4. + prBalance, # 5. + lpEm, # 6. + lpId, # 7. + sts, # 8. + commonState, # 9. + amDiff, # 10. + prDiff, # 11. + inAmId, # 12 + inPrId # 13 + ) +} + +func getYD(poolAddress: Address, xp: List[BigInt], i: Int, D: BigInt) = { + let n = big2 + let x = xp[if (i == 0) then 1 else 0] + let aPrecision = Amult.parseBigIntValue() + let a = A(poolAddress).parseBigIntValue() * aPrecision + let s = x + let ann = a * n + let c = D * D / (x * n) * D * aPrecision / (ann * n) + let b = s + D * aPrecision / ann - D + func calc(acc: (BigInt, Int|Unit), cur: Int) = { + let (y, found) = acc + if (found != unit) then acc else { + let yNext = (y * y + c) / (big2 * y + b) + let yDiff = absBigInt(yNext - y.value()) + if (yDiff <= big1) then { + (yNext, cur) + } else { + (yNext, unit) + } + } + } + let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + let (y, found) = FOLD<15>(arr, (D, unit), calc) + if (found != unit) then { + y + } else { + { "Y calculation error, Y = " + y.toString() }.throw() + } +} + +func calcDLp(poolAddress: Address, amountBalance: BigInt, priceBalance: BigInt, lpEmission: BigInt) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let updatedDLp = fraction( + getD(poolAddress, + [ + amountBalance.t1BigInt(cfgAmountAssetDecimals.toBigInt()), + priceBalance.t1BigInt(cfgPriceAssetDecimals.toBigInt()) + ]), + scale18, + lpEmission + ) + + if (lpEmission == big0) then big0 else updatedDLp +} + +func calcCurrentDLp(poolAddress: Address, amountAssetDelta: BigInt, priceAssetDelta: BigInt, lpAssetEmissionDelta: BigInt) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let amountAssetBalance = getAccBalance(poolAddress, cfgAmountAssetId.assetIdToString()).toBigInt() - amountAssetDelta + let priceAssetBalance = getAccBalance(poolAddress, cfgPriceAssetId.assetIdToString()).toBigInt() - priceAssetDelta + let lpAssetEmission = assetInfo(cfgLpAssetId).value().quantity.toBigInt() - lpAssetEmissionDelta + + let currentDLp = calcDLp(poolAddress, amountAssetBalance, priceAssetBalance, lpAssetEmission) + + currentDLp +} + +func refreshDLpInternal(poolAddress: Address, amountAssetBalanceDelta: Int, priceAssetBalanceDelta: Int, lpAssetEmissionDelta: Int) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let amountAssetBalance = getAccBalance(poolAddress, cfgAmountAssetId.assetIdToString()) + amountAssetBalanceDelta + let priceAssetBalance = getAccBalance(poolAddress, cfgPriceAssetId.assetIdToString()) + priceAssetBalanceDelta + # TODO: can be moved outside functions + let lpAssetEmission = assetInfo(cfgLpAssetId).value().quantity + lpAssetEmissionDelta + + let updatedDLp = calcDLp(poolAddress, amountAssetBalance.toBigInt(), priceAssetBalance.toBigInt(), lpAssetEmission.toBigInt()) + + strict actions = [ + poolAddress.invoke("integerEntry", [keyDLpRefreshedHeight, height], []), + poolAddress.invoke("stringEntry", [keyDLp, updatedDLp.toString()], []) + ] + + ([], updatedDLp) +} + +func validateUpdatedDLp(oldDLp: BigInt, updatedDLp: BigInt) = { + updatedDLp >= oldDLp || "updated DLp lower than current DLp".throwErr() +} + +func validateMatcherOrderAllowed(poolAddress: Address, order: Order) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let amountAssetBalance = getAccBalance(poolAddress, cfgAmountAssetId.assetIdToString()) + let priceAssetBalance = getAccBalance(poolAddress, cfgPriceAssetId.assetIdToString()) + let amountAssetAmount = order.amount + # TODO: check order.priceMode when available + let priceAssetAmount = fraction(order.amount, order.price, scale8, FLOOR) + let (amountAssetBalanceDelta, priceAssetBalanceDelta) = if (order.orderType == Buy) then { + (amountAssetAmount, -priceAssetAmount) + } else { + (-amountAssetAmount, priceAssetAmount) + } + + # validate status + if (igs() || cfgPoolStatus == PoolMatcherDis || cfgPoolStatus == PoolShutdown) then throw("Admin blocked") else + + # validate pairs + if (order.assetPair.amountAsset != cfgAmountAssetId || order.assetPair.priceAsset != cfgPriceAssetId) then throw("Wr assets") else + + let dLp = this.getString(keyDLp).valueOrElse("0").parseBigIntValue() + let (unusedActions, dLpNew) = refreshDLpInternal(poolAddress, amountAssetBalanceDelta, priceAssetBalanceDelta, 0) + let isOrderValid = dLpNew >= dLp + + let info = [ + "dLp=", dLp.toString(), + " dLpNew=", dLpNew.toString(), + " amountAssetBalance=", amountAssetBalance.toString(), + " priceAssetBalance=", priceAssetBalance.toString(), + " amountAssetBalanceDelta=", amountAssetBalanceDelta.toString(), + " priceAssetBalanceDelta=", priceAssetBalanceDelta.toString(), + " height=", height.toString() + ].makeString("") + + (isOrderValid, info) +} + +# commonGet +func cg(poolAddress: Address, readonly: Boolean, pmt: AttachedPayment, i: Invocation) = { + let userAddress = i.originCaller + + let pmtAssetId = pmt.assetId.value() + let pmtAmt = pmt.amount + + let r = ego(poolAddress, readonly, i.transactionId.toBase58String(), pmtAssetId.toBase58String(), pmtAmt, i.originCaller) + let outAmAmt = r._1 + let outPrAmt = r._2 + let sts = r._9.parseIntValue() + let state = r._10 + + let isGetDisabled = !isAddressWhitelisted(userAddress) && (igs() || sts == PoolShutdown) + if (isGetDisabled) then throw("Admin blocked: " + sts.toString()) else + + (outAmAmt, + outPrAmt, + pmtAmt, + pmtAssetId, + state + ) +} + +# commonPut +func cp(poolAddress: Address, + readonly: Boolean, + caller: String, + txId: String, + amAsPmt: AttachedPayment, + prAsPmt: AttachedPayment, + slippage: Int, + emitLp: Boolean, + isOneAsset: Boolean, + validateSlippage: Boolean, + pmtAmt: Int, + pmtId: String) = { + let r = epo(poolAddress, + readonly, + txId, # i.transactionId.toBase58String() + slippage, + amAsPmt.value().amount, + amAsPmt.value().assetId, + prAsPmt.value().amount, + prAsPmt.value().assetId, + caller, #i.caller.toString() + txId == "", + emitLp, + isOneAsset, + validateSlippage, + pmtAmt, + pmtId) + + let sts = r._8.parseIntValue() + + let isPutDisabled = !isAddressWhitelisted(caller.addressFromStringValue()) && (igs() || sts == PoolShutdown || sts == PoolPutDis) + if (isPutDisabled) then throw("Blocked:" + sts.toString()) else + + r +} + +func calcPutOneTkn( + poolAddress: Address, + pmtAmtRaw: Int, + pmtAssetId: String, + userAddress: String, + txId: String, + withTakeFee: Boolean +) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let amId = cfgAmountAssetId.value().toBase58String() + let prId = cfgPriceAssetId.value().toBase58String() + let lpId = cfgLpAssetId + let amtDcm = cfgAmountAssetDecimals + let priceDcm = cfgPriceAssetDecimals + + let lpAssetEmission = lpId.assetInfo().valueOrErrorMessage("invalid lp asset").quantity.toBigInt() + strict chechEmission = lpAssetEmission > big0 || "initial deposit requires all coins".throw() + + let amBalance = getAccBalance(poolAddress, amId) + let prBalance = getAccBalance(poolAddress, prId) + + let (amBalanceOld, prBalanceOld) = + if (txId == "") then (amBalance, prBalance) else { + if (pmtAssetId == amId) then { + if (amBalance < pmtAmtRaw) then "invalid payment amount".throw() else + (amBalance - pmtAmtRaw, prBalance) + } else if (pmtAssetId == prId) then { + if (prBalance < pmtAmtRaw) then "invalid payment amount".throw() else + (amBalance, prBalance - pmtAmtRaw) + } else "wrong pmtAssetId".throw() + } + + let (amAmountRaw, prAmountRaw) = if (pmtAssetId == amId) then { + (pmtAmtRaw, 0) + } else if (pmtAssetId == prId) then { + (0, pmtAmtRaw) + } else "invalid payment".throw() + + let (amAmount, prAmount, feeAmount) = if (withTakeFee) then { + ( + amAmountRaw.takeFee(inFee)._1, + prAmountRaw.takeFee(inFee)._1, + pmtAmtRaw.takeFee(inFee)._2 + ) + } else { + ( + amAmountRaw, + prAmountRaw, + 0 + ) + } + + let amBalanceNew = amBalanceOld + amAmount + let prBalanceNew = prBalanceOld + prAmount + + # check D1 > D0 + let D0 = getD(poolAddress, [amBalanceOld.t1(cfgAmountAssetDecimals), prBalanceOld.t1(cfgPriceAssetDecimals)]) + let D1 = getD(poolAddress, [amBalanceNew.t1(cfgAmountAssetDecimals), prBalanceNew.t1(cfgPriceAssetDecimals)]) + + strict checkD = D1 > D0 || throw() + + let lpAmount = lpAssetEmission.fraction(D1 - D0, D0, FLOOR) + + let curPrice = cpbi(prBalanceNew.t1(priceDcm), amBalanceNew.t1(amtDcm)).f1(scale8) + + strict actions = [ + poolAddress.invoke("integerEntry", [pl(), curPrice], []), + poolAddress.invoke("integerEntry", [ph(height, lastBlock.timestamp), curPrice], []), + poolAddress.invoke("stringEntry", [ + pau(userAddress, txId), + dataPutActionInfo(amAmountRaw, prAmountRaw, lpAmount.toInt(), curPrice, 0, 0, height, lastBlock.timestamp, 0, 0) + ], []) + ] + + let commonState = [] + + let poolProportion = prBalanceOld.fraction(scale8, amBalanceOld) + let amountAssetPart = fraction(pmtAmtRaw, scale8, poolProportion + scale8) + let priceAssetPart = pmtAmtRaw - amountAssetPart + let lpAmtBoth = fraction(lpAssetEmission, priceAssetPart.toBigInt(), prBalanceOld.toBigInt()) + let bonus = fraction(lpAmount - lpAmtBoth, scale8BigInt, lpAmtBoth).toInt() + + (lpAmount.toInt(), commonState, feeAmount, bonus) +} + +func getOneTknV2Internal( + poolAddress: Address, + outAssetId: String, + minOutAmount: Int, + payments: List[AttachedPayment], + userAddress: Address, + transactionId: ByteVector +) = { + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let lpId = cfgLpAssetId.value().toBase58String() + let amId = cfgAmountAssetId.value().toBase58String() + let prId = cfgPriceAssetId.value().toBase58String() + let amDecimals = cfgAmountAssetDecimals + let prDecimals = cfgPriceAssetDecimals + let poolStatus = cfgPoolStatus + + let pmt = payments[0].value() + let pmtAssetId = pmt.assetId.value() + let pmtAmt = pmt.amount + + strict currentDLp = calcCurrentDLp(poolAddress, big0, big0, big0) + + let txId58 = transactionId.toBase58String() + + if (lpId != pmtAssetId.toBase58String()) then throw("Wrong LP") else + + strict amBalance = getAccBalance(poolAddress, amId) + strict prBalance = getAccBalance(poolAddress, prId) + + strict (totalGet, feeAmount) = this.invoke( + "getOneTknV2READONLY", + [poolAddress.toString(), outAssetId, pmtAmt], + [] + ).exactAs[(Int, Int)] + + let totalAmount = if (minOutAmount > 0 && totalGet < minOutAmount) then { + ["amount to receive is less than ", minOutAmount.toString()].makeString("").throwErr() + } else totalGet + + let (outAm, outPr, amBalanceNew, prBalanceNew) = if (outAssetId == amId) then { + (totalAmount, 0, amBalance - totalAmount - feeAmount, prBalance) + } else if (outAssetId == prId) then { + (0, totalAmount, amBalance, prBalance - totalAmount - feeAmount) + } else { + "invalid out asset id".throw() + } + + let curPrX18 = cpbi(prBalanceNew.t1(prDecimals), amBalanceNew.t1(amDecimals)) + let curPr = curPrX18.f1(scale8) + + let outAssetIdOrWaves = if (outAssetId == wavesString) then unit else outAssetId.fromBase58String() + strict sendFeeToMatcher = if (feeAmount > 0) then + strict tr = [ + poolAddress.invoke( + "transferAsset", + [feeCollectorAddress.bytes, feeAmount, outAssetId], + [] + )] + [] + else [] + + strict stateInvokes = [ + poolAddress.invoke( + "transferAsset", + [userAddress.bytes, totalAmount, outAssetId], + [] + ), + poolAddress.invoke( + "stringEntry", + [ + gau(userAddress.toString(), txId58), + dataGetActionInfo(outAm, outPr, pmtAmt, curPr, height, lastBlock.timestamp) + ], + [] + ), + poolAddress.invoke( + "integerEntry", + [pl(), curPr], + [] + ), + poolAddress.invoke( + "integerEntry", + [ph(height, lastBlock.timestamp), curPr], + [] + ) + ] + strict state = [] ++ sendFeeToMatcher + + strict burn = poolAddress.invoke( + "callBurn", + [fca.bytes, pmtAssetId.assetIdToString(), pmtAmt], + [] + ) + + let (amountAssetBalanceDelta, priceAssetBalanceDelta) = { + let feeAmountForCalc = if (this == feeCollectorAddress) then 0 else feeAmount + let outInAmountAsset = if (outAssetId.parseAssetId() == cfgAmountAssetId) then true else false + if (outInAmountAsset) then (-(totalGet + feeAmountForCalc), 0) else (0, -(totalGet + feeAmountForCalc)) + } + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + + strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + + (state ++ refreshDLpActions, totalAmount) +} + +# managerPublicKeyOrUnit +func managerPublicKeyOrUnit() = { + let managerVaultAddress = getManagerVaultAddressOrThis() + match managerVaultAddress.getString(keyManagerPublicKey()) { + case s: String => s.fromBase58String() + case _: Unit => unit + } +} + +let pd = "Permission denied".throw() + +func isManager(i: Invocation) = { + match managerPublicKeyOrUnit() { + case pk: ByteVector => i.originCaller == addressFromPublicKey(pk) + case _: Unit => i.caller == this + } +} + +# mustManager +func mustManager(i: Invocation) = { + match managerPublicKeyOrUnit() { + case pk: ByteVector => i.originCaller == addressFromPublicKey(pk) || pd + case _: Unit => i.caller == this || pd + } +} + +func getY(poolAddress: Address, isReverse: Boolean, D: BigInt, poolAmountInBalance: BigInt) = { + let poolConfig = gpc(poolAddress) + let amId = poolConfig[idxAmAsId] + let prId = poolConfig[idxPrAsId] + let n = big2 + let aPrecision = Amult.parseBigIntValue() + let a = A(poolAddress).parseBigIntValue() * aPrecision + let xp = if (isReverse == false) then { + [getAccBalance(poolAddress, amId).toBigInt() + poolAmountInBalance, getAccBalance(poolAddress, prId).toBigInt()] + } else { + [getAccBalance(poolAddress, prId).toBigInt() + poolAmountInBalance, getAccBalance(poolAddress, amId).toBigInt()] + } + + let x = xp[0] + let s = x + let ann = a * n + + let c = D * D / (x * n) * D * aPrecision / (ann * n) + let b = s + D * aPrecision / ann - D + func calc(acc: (BigInt, Int|Unit), cur: Int) = { + let (y, found) = acc + if (found != unit) then acc else { + let yNext = (y * y + c) / (big2 * y + b) + let yDiff = absBigInt(yNext - y.value()) + if (yDiff <= big1) then { + (yNext, cur) + } else { + (yNext, unit) + } + } + } + let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + let (y, found) = FOLD<15>(arr, (D, unit), calc) + if (found != unit) then { + y + } else { + { "Y calculation error, Y = " + y.toString() }.throw() + } +} + +func skipOrderValidation() = { + fca.getBoolean(keySkipOrderValidation(this.toString())).valueOrElse(false) +} + +@Callable(i) +func calculateAmountOutForSwapREADONLY(cleanAmountIn: Int, isReverse: Boolean, feePoolAmount: Int) = { + let poolAddress = i.caller + + let (assetOut, poolAmountInBalance) = if (isReverse == false) then { + let assetOut = poolAddress.strf(pa()) + let poolAmountInBalance = getAccBalance(poolAddress, poolAddress.strf(aa())).toBigInt() + cleanAmountIn.toBigInt() + (assetOut, poolAmountInBalance) + } else { + let assetOut = poolAddress.strf(aa()) + let poolAmountInBalance = getAccBalance(poolAddress, poolAddress.strf(pa())).toBigInt() + cleanAmountIn.toBigInt() + (assetOut, poolAmountInBalance) + } + + let poolConfig = gpc(poolAddress) + let amId = poolConfig[idxAmAsId] + let prId = poolConfig[idxPrAsId] + let xp = [getAccBalance(poolAddress, amId).toBigInt(), getAccBalance(poolAddress, prId).toBigInt()] + + let D = getD(poolAddress, xp) + let y = getY(poolAddress, isReverse, D, cleanAmountIn.toBigInt()) + + let dy = getAccBalance(poolAddress, assetOut).toBigInt() - y - toBigInt(1) # -1 just in case there were some rounding errors + + let totalGetRaw = [0, dy.toInt()].max() + + let newXp = if (isReverse == false) then { + [getAccBalance(poolAddress, amId).toBigInt() + cleanAmountIn.toBigInt() + feePoolAmount.toBigInt(), getAccBalance(poolAddress, prId).toBigInt() - dy] + } else { + [getAccBalance(poolAddress, amId).toBigInt() - dy, getAccBalance(poolAddress, prId).toBigInt() + cleanAmountIn.toBigInt() + feePoolAmount.toBigInt()] + } + let newD = getD(poolAddress, newXp) + strict checkD = newD >= D || makeString(["new D is fewer error", D.toString(), newD.toString()], "__").throw() + + (nil, totalGetRaw) +} + +@Callable(i) +func calculateAmountOutForSwapAndSendTokens( + cleanAmountIn: Int, + isReverse: Boolean, + amountOutMin: Int, + addressTo: String, + feePoolAmount: Int, + paymentAssetId: String, + paymentAmount: Int) = { + let poolAddress = i.caller + let userAddress = i.originCaller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let swapContact = fca.invoke( + "getSwapContractREADONLY", + [], + [] + ).exactAs[String] + + let isPoolSwapDisabled = fca.invoke( + "isPoolSwapDisabledREADONLY", + [poolAddress.toString()], + [] + ).exactAs[Boolean] + let isSwapDisabled = !isAddressWhitelisted(userAddress) && (igs() || cfgPoolStatus == PoolShutdown || isPoolSwapDisabled) + + strict checks = [ + !isSwapDisabled || i.isManager() || "swap operation is blocked by admin".throwErr(), + paymentAmount >= cleanAmountIn || "Wrong amount".throwErr(), + i.originCaller == addressFromStringValue(swapContact) || "Permission denied".throwErr() + ] + let assetIn = paymentAssetId + + let (assetOut, poolAmountInBalance) = if (isReverse == false) then { + let assetOut = poolAddress.strf(pa()) + let poolAmountInBalance = getAccBalance(poolAddress, assetIn) - paymentAmount + (assetOut, poolAmountInBalance) + } else { + let assetOut = poolAddress.strf(aa()) + let poolAmountInBalance = getAccBalance(poolAddress, assetIn) - paymentAmount + (assetOut, poolAmountInBalance) + } + + let amId = cfgAmountAssetId.value().toBase58String() + let prId = cfgPriceAssetId.value().toBase58String() + let xp = if (isReverse == false) then { + [getAccBalance(poolAddress, amId).toBigInt() - paymentAmount.toBigInt(), getAccBalance(poolAddress, prId).toBigInt()] + } else { + [getAccBalance(poolAddress, amId).toBigInt(), getAccBalance(poolAddress, prId).toBigInt() - paymentAmount.toBigInt()] + } + let D = getD(poolAddress, xp) + let y = getY(poolAddress, isReverse, D, toBigInt(0)) + + let dy = getAccBalance(poolAddress, assetOut).toBigInt() - y - toBigInt(1) # -1 just in case there were some rounding errors + + let totalGetRaw = [0, dy.toInt()].max() + + strict checkMin = amountOutMin <= totalGetRaw || "Exchange result is fewer coins than expected".throw() + + let newXp = if (isReverse == false) then { + [getAccBalance(poolAddress, amId).toBigInt() + feePoolAmount.toBigInt(), getAccBalance(poolAddress, prId).toBigInt() - dy] + } else { + [getAccBalance(poolAddress, amId).toBigInt() - dy, getAccBalance(poolAddress, prId).toBigInt() + feePoolAmount.toBigInt()] + } + let newD = getD(poolAddress, newXp) + strict checkD = newD >= D || "new D is fewer error".throw() + + let amountAssetBalanceDelta = if (isReverse) then { + -totalGetRaw + } else { + feePoolAmount + } + let priceAssetBalanceDelta = if (isReverse) then { + feePoolAmount + } else { + -totalGetRaw + } + strict refreshDLpActions = refreshDLpInternal(poolAddress, amountAssetBalanceDelta, priceAssetBalanceDelta, 0)._1 + + strict transfer = poolAddress.invoke( + "transferAsset", + [addressTo.fromBase58String(), totalGetRaw, assetOut], + [] + ) + ([], totalGetRaw) +} + +# called by: LP +# +# purpose: +# function used for entering the pool +# actions: +# validate list: +# 1. tokens ratio is in correct range +# 2. slipage is not bigger that current tokens ratio +# arguments: +# slippageTolerance max allowed slippage +# shouldAutoStake perform LP staking immediatelly in case true otherwise transfer LP to user) +# attach: +# attached should be two valid tokens from the available pools. +# return: +# transfer LP tokens based on deposit share +@Callable(i) +func put( + slip: Int, + autoStake: Boolean, + payment1AssetId: String, + payment1Amount: Int, + payment2AssetId: String, + payment2Amount: Int + ) = { + let poolAddress = i.caller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let factCfg = gfc() + let stakingCntr = addressFromString(factCfg[idxFactStakCntr]).valueOrErrorMessage("Wr st addr") + let slipCntr = addressFromString(factCfg[idxFactSlippCntr]).valueOrErrorMessage("Wr sl addr") + if (slip < 0) then throw("Wrong slippage") else + # if (i.payments.size() != 2) then throw("2 pmnts expd") else + + let amAssetPmt = payment1Amount.toBigInt() + let prAssetPmt = payment2Amount.toBigInt() + + strict amountAssetBalance = getAccBalance(poolAddress, cfgAmountAssetId.assetIdToString()).toBigInt() - amAssetPmt + strict priceAssetBalance = getAccBalance(poolAddress, cfgPriceAssetId.assetIdToString()).toBigInt() - prAssetPmt + strict lpAssetEmission = assetInfo(cfgLpAssetId).value().quantity.toBigInt() + strict currentDLp = calcCurrentDLp(poolAddress, amAssetPmt, prAssetPmt, 0.toBigInt()) + # estPut + let e = cp(poolAddress, + false, + i.originCaller.toString(), + i.transactionId.toBase58String(), + AttachedPayment(payment1AssetId.parseAssetId(), payment1Amount), + AttachedPayment(payment2AssetId.parseAssetId(), payment2Amount), + # i.payments[1], + slip, + true, + false, + true, + 0, + "") + + let emitLpAmt = e._2 + let lpAssetId = e._7 + let state = e._9 + let amDiff = e._10 + let prDiff = e._11 + let amId = e._12 + let prId = e._13 + + # emit lp on factory + strict r = poolAddress.invoke( "callEmit", [fca.bytes, emitLpAmt], []) + # if the lp instance address is in the legacy list then the legacy factory address will be returned from the factory + strict el = match (r) { + case legacy: Address => poolAddress.invoke("callEmit", [legacy.bytes, emitLpAmt], []) + case _ => unit + } + strict sa = if(amDiff > 0) then poolAddress.invoke("callPut",[slipCntr.bytes, amId.assetIdToString(), amDiff], []) else [] + strict sp = if(prDiff > 0) then poolAddress.invoke("callPut",[slipCntr.bytes, prId.assetIdToString(), prDiff], []) else [] + + let lpTrnsfr = + if(autoStake) then strict ss = poolAddress.invoke("callStake",[stakingCntr.bytes, lpAssetId.assetIdToString(), emitLpAmt], []); [] + else strict tr = poolAddress.invoke("transferAsset", [i.originCaller.bytes, emitLpAmt, lpAssetId.assetIdToString()], []); [] + + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + + # strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + strict check = updatedDLp >= currentDLp || [ + "updated DLp lower than current DLp", + amountAssetBalance.toString(), + priceAssetBalance.toString(), + lpAssetEmission.toString(), + currentDLp.toString(), + updatedDLp.toString(), + amDiff.toString(), + prDiff.toString() + ].makeString(" ").throwErr() + + strict lpAssetEmissionAfter = assetInfo(cfgLpAssetId).value().quantity + + state + ++ lpTrnsfr + ++ refreshDLpActions +} + +@Callable(i) +func putOneTknV2( + minOutAmount: Int, + autoStake: Boolean, + payment1AssetId: String, + payment1Amount: Int + ) = { + let poolAddress = i.caller + let userAddress = i.originCaller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let isPoolOneTokenOperationsDisabled = fca.invoke( + "isPoolOneTokenOperationsDisabledREADONLY", + [poolAddress.toString()], + [] + ).exactAs[Boolean] + let isPutDisabled = !isAddressWhitelisted(userAddress) && (igs() || cfgPoolStatus == PoolPutDis || cfgPoolStatus == PoolShutdown || isPoolOneTokenOperationsDisabled) + + strict checks = [ + !isPutDisabled || i.isManager() || "put operation is blocked by admin".throwErr() + ] + + let amId = cfgAmountAssetId.value().toBase58String() + let prId = cfgPriceAssetId.value().toBase58String() + let lpId = cfgLpAssetId + let amDecimals = cfgAmountAssetDecimals + let prDecimals = cfgPriceAssetDecimals + + let pmt = AttachedPayment(payment1AssetId.parseAssetId(), payment1Amount) + let pmtAssetId = pmt.assetId.value().toBase58String() + let pmtAmt = pmt.amount + + strict currentDLp = if (pmt.assetId == cfgAmountAssetId) then { + calcCurrentDLp(poolAddress, pmtAmt.toBigInt(), 0.toBigInt(), 0.toBigInt()) + } else { + calcCurrentDLp(poolAddress, 0.toBigInt(), pmtAmt.toBigInt(), 0.toBigInt()) + } + + strict (estimLP, state, feeAmount) = calcPutOneTkn( + poolAddress, + pmtAmt, + pmtAssetId, + userAddress.toString(), + i.transactionId.toBase58String(), + true + ) + + let emitLpAmt = if (minOutAmount > 0 && estimLP < minOutAmount) then { + ["amount to receive is less than ", minOutAmount.toString()].makeString("").throwErr() + } else estimLP + + # emit lp on factory + strict e = poolAddress.invoke("callEmit", [fca.bytes, emitLpAmt], []) + # if the lp instance address is in the legacy list then the legacy factory address will be returned from the factory + strict el = match (e) { + case legacy: Address => poolAddress.invoke("callEmit", [legacy.bytes, emitLpAmt], []) + case _ => unit + } + + let lpTrnsfr = + if (autoStake) then { + strict ss = poolAddress.invoke( + "callStakeFor", + [ + stakingContract.bytes, + i.originCaller.toString(), + lpId.assetIdToString(), + emitLpAmt + ], + [] + ) + + [] + } else { + strict lpTrInv = poolAddress.invoke( + "transferAsset", + [ + i.originCaller.bytes, + emitLpAmt, + lpId.assetIdToString() + ], + [] + ) + [] + } + + let sendFeeToMatcher = if (feeAmount > 0) then { + strict feeTrInv = poolAddress.invoke( + "transferAsset", + [ + feeCollectorAddress.bytes, + feeAmount, + pmtAssetId + ], + [] + ) + [] + } else [] + + let (amountAssetBalanceDelta, priceAssetBalanceDelta) = { + if (this == feeCollectorAddress) then (0, 0) else { + # full payment asset id validation is in calcPutOneTkn + let paymentInAmountAsset = if (pmt.assetId == cfgAmountAssetId) then true else false + if (paymentInAmountAsset) then (-feeAmount, 0) else (0, -feeAmount) + } + } + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, amountAssetBalanceDelta, priceAssetBalanceDelta, 0) + + strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + + ( + state + ++ lpTrnsfr + ++ sendFeeToMatcher + ++ refreshDLpActions, + emitLpAmt + ) +} + +# Put without LP emission +@Callable(i) +func putForFree( + maxSlpg: Int, + payment1AssetId: String, + payment1Amount: Int, + payment2AssetId: String, + payment2Amount: Int +) = { + let poolAddress = i.caller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + if(maxSlpg < 0) then throw("Wrong slpg") else + let estPut = cp(poolAddress, + false, + i.originCaller.toString(), + i.transactionId.toBase58String(), + AttachedPayment(payment1AssetId.parseAssetId(), payment1Amount), + AttachedPayment(payment2AssetId.parseAssetId(), payment2Amount), + maxSlpg, + false, + false, + true, + 0, + "") + + let state = estPut._9 + + let amAssetPmt = payment1Amount.toBigInt() + let prAssetPmt = payment2Amount.toBigInt() + + strict currentDLp = calcCurrentDLp(poolAddress, amAssetPmt, prAssetPmt, 0.toBigInt()) + + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + + strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + + state ++ refreshDLpActions +} + +# Called by: LP +# +# purpose: +# function used for exit from pool partially or fully +# actions: +# arguments: +# attach: +# attached should be corresponding pool LP token +# validate list: +# return: +# transfer to user his share of pool tokens base on passed lp token amount +@Callable(i) +func get(payment1AssetId: String, payment1Amount: Int) = { + # amountAssetBalance + # strict aab = cfgAmountAssetId.assetIdToString().getAccBalance().toBigInt() + # # priceAssetBalance + # strict pab = cfgPriceAssetId.assetIdToString().getAccBalance().toBigInt() + # # lpAssetEmission + # strict lae = assetInfo(cfgLpAssetId).value().quantity.toBigInt() + # # lpAssetEmissionAfter + # strict laea = lae - i.payments[0].value().amount.toBigInt() + let poolAddress = i.caller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + strict currentDLp = calcCurrentDLp(poolAddress, 0.toBigInt(), 0.toBigInt(), 0.toBigInt()) + + let pmt = AttachedPayment(payment1AssetId.parseAssetId(), payment1Amount) + + let r = cg(poolAddress, false, pmt, i) + let outAmtAmt = r._1 + let outPrAmt = r._2 + let pmtAmt = r._3 + let pmtAssetId = r._4 + let state = r._5 + + strict b = poolAddress.invoke( + "callBurn", + [fca.bytes, pmtAssetId.assetIdToString(), pmtAmt], + []) + + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + + strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + + state ++ refreshDLpActions +} + +@Callable(i) +func getOneTknV2( + outAssetId: String, + minOutAmount: Int, + payment1AssetId: String, + payment1Amount: Int + ) = { + let poolAddress = i.caller + let userAddress = i.originCaller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let isPoolOneTokenOperationsDisabled = fca.invoke( + "isPoolOneTokenOperationsDisabledREADONLY", + [i.caller.toString()], + [] + ).exactAs[Boolean] + let isGetDisabled = !isAddressWhitelisted(userAddress) && (igs() || cfgPoolStatus == PoolShutdown || isPoolOneTokenOperationsDisabled) + + strict checks = [ + !isGetDisabled || i.isManager() || "get operation is blocked by admin".throwErr() + ] + + let pmt = AttachedPayment(payment1AssetId.parseAssetId(), payment1Amount) + let (state, totalAmount) = getOneTknV2Internal( + poolAddress, + outAssetId, + minOutAmount, + [pmt], + i.originCaller, + i.transactionId + ) + + (state, totalAmount) +} + + +@Callable(i) +func refreshDLp() = { + let poolAddress = i.caller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let lastRefreshedBlockHeight = poolAddress.getInteger(keyDLpRefreshedHeight).valueOrElse(0) + strict checkLastRefreshedBlockHeight = if (height - lastRefreshedBlockHeight >= dLpRefreshDelay(poolAddress)) then unit else { + [ + dLpRefreshDelay(poolAddress).toString(), + " blocks have not passed since the previous call" + ].makeString("").throwErr() + } + + let dLp = poolAddress.getString(keyDLp) + .valueOrElse("0") + .parseBigInt() + .valueOrErrorMessage("invalid dLp".fmtErr()) + + let (dLpUpdateActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + let actions = if (dLp != updatedDLp) then dLpUpdateActions else "nothing to refresh".throwErr() + + (actions, updatedDLp.toString()) +} + +@Callable(i) +func getOneTknV2READONLY( + poolAddressString: String, + outAssetId: String, + lpAssetAmount: Int +) = { + let poolAddress = poolAddressString.addressFromStringValue() + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let amId = cfgAmountAssetId.value().toBase58String() + let prId = cfgPriceAssetId.value().toBase58String() + let lpId = cfgLpAssetId.value().toBase58String() + let xp = [getAccBalance(poolAddress, amId).toBigInt(), getAccBalance(poolAddress, prId).toBigInt()] + # let xp = [prId.getAccBalance().toBigInt(), amId.getAccBalance().toBigInt()] + let lpEmission = lpId.fromBase58String().assetInfo().valueOrErrorMessage("invalid lp asset").quantity.toBigInt() + let D0 = getD(poolAddress, xp) + let D1 = D0 - fraction(lpAssetAmount.toBigInt(), D0, lpEmission) + let index = if (outAssetId == amId) then { + 0 + } else if (outAssetId == prId) then { + 1 + } else { + "invalid out asset id".throw() + } + let newY = getYD(poolAddress, xp, index, D1) + let dy = xp[index] - newY + let totalGetRaw = [0, { dy - big1 }.toInt()].max() + let (totalGet, feeAmount) = totalGetRaw.takeFee(outFee) + + (nil, (totalGet, feeAmount)) +} + +@Callable(i) +func getOneTknV2WithBonusREADONLY( + outAssetId: String, + lpAssetAmount: Int +) = { + let poolAddress = i.caller + let userAddress = i.originCaller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let amId = cfgAmountAssetId.value().toBase58String() + let prId = cfgPriceAssetId.value().toBase58String() + let lpId = cfgLpAssetId.value().toBase58String() + + let amBalance = getAccBalance(poolAddress, amId) + let prBalance = getAccBalance(poolAddress, prId) + + let (totalGet, feeAmount) = this.invoke( + "getOneTknV2READONLY", + [poolAddress.toString(), outAssetId, lpAssetAmount], + [] + ).exactAs[(Int, Int)] + + let r = ego( + poolAddress, + true, + "", + lpId, + lpAssetAmount, + userAddress + ) + + let outAmAmt = r._1 + let outPrAmt = r._2 + let sumOfGetAssets = outAmAmt + outPrAmt + + let bonus = if (sumOfGetAssets == 0) then { + if (totalGet == 0) then 0 else "bonus calculation error".throw() + } else { + fraction((totalGet - sumOfGetAssets), scale8, sumOfGetAssets) + } + + ([], (totalGet, feeAmount, bonus)) +} + +@Callable(i) +func getNoLess( + noLessThenAmtAsset: Int, + noLessThenPriceAsset: Int, + payment1AssetId: String, + payment1Amount: Int +) = { + let poolAddress = i.caller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let pmt = AttachedPayment(payment1AssetId.parseAssetId(), payment1Amount) + + let r = cg(poolAddress, false, pmt, i) + let outAmAmt = r._1 + let outPrAmt = r._2 + let pmtAmt = r._3 + let pmtAssetId = r._4 + let state = r._5 + if (outAmAmt < noLessThenAmtAsset) then throw("Failed: " + outAmAmt.toString() + " < " + noLessThenAmtAsset.toString()) else + if (outPrAmt < noLessThenPriceAsset) then throw("Failed: " + outPrAmt.toString() + " < " + noLessThenPriceAsset.toString()) else + + strict currentDLp = calcCurrentDLp(poolAddress, 0.toBigInt(), 0.toBigInt(), 0.toBigInt()) + + strict burnLPAssetOnFactory = poolAddress.invoke( + "callBurn", + [fca.bytes, pmtAssetId.assetIdToString(), pmtAmt], + []) + + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + + strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + + state ++ refreshDLpActions +} + +# Unstake LP tokens and exit from pool +@Callable(i) +func unstakeAndGet(amount: Int) = { + let poolAddress = i.caller + let userAddress = i.originCaller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let factoryCfg = gfc() + + let lpAssetId = cfgLpAssetId + let staking = factoryCfg[idxFactStakCntr].addressFromString().valueOrErrorMessage("Wr st addr") + + strict currentDLp = calcCurrentDLp(poolAddress, 0.toBigInt(), 0.toBigInt(), 0.toBigInt()) + + # negative amount will not pass + strict unstakeInv = poolAddress.invoke("callUnstake", [stakingContract.bytes, lpAssetId.assetIdToString(), amount], []); + + let r = ego(poolAddress, false, i.transactionId.toBase58String(), lpAssetId.toBase58String(), amount, i.originCaller) + let outAmAmt = r._1 + let outPrAmt = r._2 + let sts = r._9.parseIntValue() + let state = r._10 + + let isGetDisabled = !isAddressWhitelisted(userAddress) && (igs() || cfgPoolStatus == PoolShutdown) + strict v = if (isGetDisabled) then throw("Blocked: " + sts.toString()) else true + + strict burnLPAssetOnFactory = poolAddress.invoke( + "callBurn", + [fca.bytes, lpAssetId.assetIdToString(), amount], + [] + ) + + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + + strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + + state ++ refreshDLpActions +} + +@Callable(i) +func unstakeAndGetNoLess(unstakeAmount: Int, noLessThenAmountAsset: Int, noLessThenPriceAsset: Int) = { + let poolAddress = i.caller + let userAddress = i.originCaller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let isGetDisabled = !isAddressWhitelisted(userAddress) && (igs() || cfgPoolStatus == PoolShutdown) + + strict checks = [ + !isGetDisabled || "get operation is blocked by admin".throw() + ] + + strict currentDLp = calcCurrentDLp(poolAddress, 0.toBigInt(), 0.toBigInt(), 0.toBigInt()) + + strict unstakeInv = poolAddress.invoke("callUnstake", [stakingContract.bytes, cfgLpAssetId.assetIdToString(), unstakeAmount], []); + + let res = ego(poolAddress, false, i.transactionId.toBase58String(), cfgLpAssetId.toBase58String(), unstakeAmount, i.originCaller) + let outAmAmt = res._1 + let outPrAmt = res._2 + let state = res._10 + + strict checkAmounts = [ + outAmAmt >= noLessThenAmountAsset || ["amount asset amount to receive is less than ", noLessThenAmountAsset.toString()].makeString("").throw(), + outPrAmt >= noLessThenPriceAsset || ["price asset amount to receive is less than ", noLessThenPriceAsset.toString()].makeString("").throw() + ] + + strict burnLPAssetOnFactory = poolAddress.invoke( + "callBurn", + [fca.bytes, cfgLpAssetId.assetIdToString(), unstakeAmount], + [] + ) + + let (refreshDLpActions, updatedDLp) = refreshDLpInternal(poolAddress, 0, 0, 0) + + strict isUpdatedDLpValid = validateUpdatedDLp(currentDLp, updatedDLp) + + state ++ refreshDLpActions +} + +@Callable(i) +func unstakeAndGetOneTknV2(unstakeAmount: Int, outAssetId: String, minOutAmount: Int) = { + let poolAddress = i.caller + let userAddress = i.originCaller + let poolConfigParsed = gpc(poolAddress).parsePoolConfig() + let ( + cfgPoolAddress, + cfgPoolStatus, + cfgLpAssetId, + cfgAmountAssetId, + cfgPriceAssetId, + cfgAmountAssetDecimals, + cfgPriceAssetDecimals + ) = poolConfigParsed + + let isPoolOneTokenOperationsDisabled = fca.invoke( + "isPoolOneTokenOperationsDisabledREADONLY", + [poolAddress.toString()], + [] + ).exactAs[Boolean] + let isGetDisabled = !isAddressWhitelisted(userAddress) && (igs() || cfgPoolStatus == PoolShutdown || isPoolOneTokenOperationsDisabled) + + strict checks = [ + !isGetDisabled || i.isManager() || "get operation is blocked by admin".throwErr(), + i.payments.size() == 0 || "no payments are expected".throwErr() + ] + + let factoryCfg = gfc() + + let lpAssetId = cfgLpAssetId + let staking = factoryCfg[idxFactStakCntr].addressFromString().valueOrErrorMessage("Wr st addr") + + let lpAssetRecipientAddress = poolAddress + strict unstakeInv = poolAddress.invoke("callUnstakeINTERNAL", [ + staking.bytes, + lpAssetId, + unstakeAmount, + userAddress.bytes, + lpAssetRecipientAddress.bytes + ], []) + let (state, totalAmount) = getOneTknV2Internal( + poolAddress, + outAssetId, + minOutAmount, + [AttachedPayment(lpAssetId, unstakeAmount)], + userAddress, + i.transactionId + ) + + (state, totalAmount) +} + +@Callable(i) +func putOneTknV2WithBonusREADONLY(paymentAmountRaw: Int, paymentAssetId: String) = { + let poolAddress = i.caller + let (lpAmount, state, feeAmount, bonus) = calcPutOneTkn(poolAddress, paymentAmountRaw, paymentAssetId, "", "", true) + + (nil, (lpAmount, feeAmount, bonus)) +} + +@Callable(i) +func putOneTknV2WithoutTakeFeeREADONLY(paymentAmountRaw: Int, paymentAssetId: String) = { + let poolAddress = i.caller + let (lpAmount, state, feeAmount, bonus) = calcPutOneTkn(poolAddress, paymentAmountRaw, paymentAssetId, "", "", false) + + (nil, (lpAmount, feeAmount, bonus)) +} + +# purpose: +# used BY FACTORY for activating new LP pool. Validate it was called only once. +# actions: +# 1. issue new LP token and save data in state +# 2. burn LP token +# 3. write initial price, that is used for first deposit +# arguments: +# attach: +# return: +@Callable(i) +func activate(amtAsStr: String, prAsStr: String) = { + if (i.caller.toString() != fca.toString()) then throw("denied") else { + ([ + StringEntry(aa(),amtAsStr), + StringEntry(pa(),prAsStr), + StringEntry(amp(), ampInitial.toString()), + StringEntry(keyAmpHistory(height), ampInitial.toString()) + ], + "success") + } +} + +# API wrappers +@Callable(i) +func getPoolConfigWrapperREADONLY() = { + let poolAddress = i.caller + ( + [], + gpc(poolAddress) + ) +} + +@Callable(i) +func getAccBalanceWrapperREADONLY(assetId: String) = { + let poolAddress = i.caller + ( + [], + getAccBalance(poolAddress, assetId) + ) +} + +@Callable(i) +func calcPricesWrapperREADONLY(amAmt: Int, prAmt: Int, lpAmt: Int) = { + let poolAddress = i.caller + let pr = calcPrices(poolAddress, amAmt, prAmt, lpAmt) + ( + [], + [ + pr[0].toString(), + pr[1].toString(), + pr[2].toString() + ] + ) +} + +@Callable(i) +func fromX18WrapperREADONLY(val: String, resScaleMult: Int) = { + ( + [], + f1(val.parseBigIntValue(), resScaleMult) + ) +} + +@Callable(i) +func toX18WrapperREADONLY(origVal: Int, origScaleMult: Int) = { + ( + [], + t1(origVal, origScaleMult).toString() + ) +} + +@Callable(i) +func calcPriceBigIntWrapperREADONLY(prAmtX18: String, amAmtX18: String) = { + ( + [], + cpbi(prAmtX18.parseBigIntValue(), amAmtX18.parseBigIntValue()).toString() + ) +} + +@Callable(i) +func estimatePutOperationWrapperREADONLY( + txId58: String, + slippage: Int, + inAmAmt: Int, + inAmId: ByteVector, + inPrAmt: Int, + inPrId: ByteVector, + usrAddr: String, + isEval: Boolean, + emitLp: Boolean +) = { + let poolAddress = i.caller + ( + [], + epo( + poolAddress, + true, + txId58, + slippage, + inAmAmt, + inAmId, + inPrAmt, + inPrId, + usrAddr, + isEval, + emitLp, + true, + false, + 0, + "" + ) + ) +} + +@Callable(i) +func estimateGetOperationWrapperREADONLY(txId58: String, pmtAsId: String, pmtLpAmt: Int, usrAddr: String) = { + let poolAddress = i.caller + let r = ego( + poolAddress, + true, + txId58, + pmtAsId, + pmtLpAmt, + usrAddr.addressFromStringValue() + ) + ( + [], + (r._1, r._2, r._3, r._4, r._5, r._6, r._7, r._8.toString(), r._9, r._10) + ) +} + +@Callable(i) +func changeAmp() = { + let cfg = fca.invoke("getChangeAmpConfigREADONLY", [this.toString()], []) + let (delay, delta, target) = match cfg { + case list: List[Any] => { + (list[0].exactAs[Int], list[1].exactAs[Int], list[2].exactAs[Int]) + } + case _ => "invalid entry type".throwErr() + } + + let curAmp = amp().getStringValue().parseIntValue() + let newAmpRaw = curAmp + delta + # to not increment/decrement too much + let newAmp = if (delta < 0) then { + if (newAmpRaw < target) then target else newAmpRaw + } else { + if (newAmpRaw > target) then target else newAmpRaw + } + + let lastCall = keyChangeAmpLastCall().getInteger().valueOrElse(0) + let wait = lastCall+delay + + strict checks = [ + height > wait || "try again in few blocks".throwErr(), + curAmp != newAmp || "already reached target".throwErr() + ] + + [ + IntegerEntry(keyChangeAmpLastCall(), height), + StringEntry(amp(), newAmp.toString()), + StringEntry(keyAmpHistory(height), newAmp.toString()) + ] +} diff --git a/test/components/lp_stable/_hooks.mjs b/test/components/lp_stable/_hooks.mjs index accc7bb84..ecdbcd8d9 100644 --- a/test/components/lp_stable/_hooks.mjs +++ b/test/components/lp_stable/_hooks.mjs @@ -19,16 +19,18 @@ const seedWordsCount = 5; const ridePath = '../ride'; const mockRidePath = 'components/lp_stable/mock'; const lpStablePath = format({ dir: ridePath, base: 'lp_stable.ride' }); +const lpStableImplPath = format({ dir: ridePath, base: 'lp_stable_impl.ride' }); const factoryV2Path = format({ dir: ridePath, base: 'factory_v2.ride' }); const stakingPath = format({ dir: mockRidePath, base: 'staking.mock.ride' }); const slippagePath = format({ dir: mockRidePath, base: 'slippage.mock.ride' }); const assetsStorePath = format({ dir: mockRidePath, base: 'assets_store.mock.ride' }); const gwxRewardPath = format({ dir: mockRidePath, base: 'gwx_reward.mock.ride' }); +const swapPath = format({ dir: mockRidePath, base: 'swap.mock.ride' }); const restPath = format({ dir: ridePath, base: 'rest.ride' }); export const mochaHooks = { async beforeAll() { - const names = ['lpStable', 'factoryV2', 'staking', 'slippage', 'gwxReward', 'manager', 'store', 'feeCollector', 'rest', 'user1']; + const names = ['lpStable', 'lpStableImpl', 'factoryV2', 'staking', 'slippage', 'gwxReward', 'manager', 'store', 'feeCollector', 'rest', 'swap', 'user1']; this.accounts = Object.fromEntries(names.map((item) => [item, randomSeed(seedWordsCount)])); const seeds = Object.values(this.accounts); const amount = 1e10; @@ -40,11 +42,13 @@ export const mochaHooks = { await waitForTx(massTransferTx.id, { apiBase }); await setScriptFromFile(lpStablePath, this.accounts.lpStable); + await setScriptFromFile(lpStableImplPath, this.accounts.lpStableImpl); await setScriptFromFile(factoryV2Path, this.accounts.factoryV2); await setScriptFromFile(stakingPath, this.accounts.staking); await setScriptFromFile(slippagePath, this.accounts.slippage); await setScriptFromFile(assetsStorePath, this.accounts.store); await setScriptFromFile(gwxRewardPath, this.accounts.gwxReward); + await setScriptFromFile(swapPath, this.accounts.swap); await setScriptFromFile(restPath, this.accounts.rest); const usdnIssueTx = issue({ @@ -60,7 +64,7 @@ export const mochaHooks = { const usdnAmount = 1e16; const massTransferTxUSDN = massTransfer({ - transfers: names.slice(-1).map((name) => ({ + transfers: names.slice(-2).map((name) => ({ recipient: address(this.accounts[name], chainId), amount: usdnAmount, })), assetId: this.usdnAssetId, @@ -82,7 +86,7 @@ export const mochaHooks = { const usdtAmount = 1e16; const massTransferTxUSDT = massTransfer({ - transfers: names.slice(-1).map((name) => ({ + transfers: names.slice(-2).map((name) => ({ recipient: address(this.accounts[name], chainId), amount: usdtAmount, })), assetId: this.usdtAssetId, @@ -242,6 +246,16 @@ export const mochaHooks = { key: '%s__managerPublicKey', type: 'string', value: publicKey(this.accounts.manager), + }, + { + key: '%s__lpStableImpl', + type: 'string', + value: address(this.accounts.lpStableImpl, chainId), + }, + { + key: '%s__swapContract', + type: 'string', + value: address(this.accounts.swap, chainId), }], chainId, }, this.accounts.factoryV2); @@ -314,10 +328,36 @@ export const mochaHooks = { key: '%s__managerPublicKey', type: 'string', value: publicKey(this.accounts.manager), + }, + { + key: '%s__factoryContract', + type: 'string', + value: address(this.accounts.factoryV2, chainId), + }, { + key: '%s__refreshDLpDelay', + type: 'integer', + value: 2, }], chainId, }, this.accounts.lpStable); await api.transactions.broadcast(setManagerLpStableTx, {}); await waitForTx(setManagerLpStableTx.id, { apiBase }); + + const setLpStableImplStateTx = data({ + additionalFee: 4e5, + data: [{ + key: '%s__factoryContract', + type: 'string', + value: address(this.accounts.factoryV2, chainId), + }, + { + key: '%s__swap', + type: 'string', + value: address(this.accounts.factoryV2, chainId), + }], + chainId, + }, this.accounts.lpStableImpl); + await api.transactions.broadcast(setLpStableImplStateTx, {}); + await waitForTx(setLpStableImplStateTx.id, { apiBase }); }, }; diff --git a/test/components/lp_stable/calcPriceBigIntWrapperREADONLY.mjs b/test/components/lp_stable/calcPriceBigIntWrapperREADONLY.mjs new file mode 100644 index 000000000..a86b44265 --- /dev/null +++ b/test/components/lp_stable/calcPriceBigIntWrapperREADONLY.mjs @@ -0,0 +1,56 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: calcPriceBigIntWrapperREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully calcPriceBigIntWrapperREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr1 = `calcPriceBigIntWrapperREADONLY("12345678000", "10000000000")`; /* eslint-disable-line */ + const response1 = await api.utils.fetchEvaluate( + lpStable, + expr1, + ); + const evaluateData1 = response1.result.value._2; /* eslint-disable-line */ + + expect(evaluateData1).to.eql({ + type: 'String', + value: '1234567800000000000', + }); + }, + ); +}); diff --git a/test/components/lp_stable/calcPricesWrapperREADONLY.mjs b/test/components/lp_stable/calcPricesWrapperREADONLY.mjs new file mode 100644 index 000000000..4b4abee8d --- /dev/null +++ b/test/components/lp_stable/calcPricesWrapperREADONLY.mjs @@ -0,0 +1,69 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: calcPricesWrapperREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully calcPricesWrapperREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr1 = `calcPricesWrapperREADONLY(1000000, 1100000, 100000000)`; /* eslint-disable-line */ + const response1 = await api.utils.fetchEvaluate( + lpStable, + expr1, + ); + const evaluateData1 = response1.result.value._2; /* eslint-disable-line */ + + expect(evaluateData1).to.eql({ + type: 'Array', + value: [ + { + type: 'String', + value: '1100000000000000000', + }, + { + type: 'String', + value: '1000000000000000000', + }, + { + type: 'String', + value: '1100000000000000000', + }, + ], + }); + }, + ); +}); diff --git a/test/components/lp_stable/calculateAmountOutForSwapAndSendTokens.mjs b/test/components/lp_stable/calculateAmountOutForSwapAndSendTokens.mjs new file mode 100644 index 000000000..7e2381333 --- /dev/null +++ b/test/components/lp_stable/calculateAmountOutForSwapAndSendTokens.mjs @@ -0,0 +1,87 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; +const api = create(apiBase); + +describe('lp_stable: calculateAmountOutForSwapAndSendTokens.mjs', /** @this {MochaSuiteModified} */() => { + it('should successfully calculateAmountOutForSwapAndSendTokens', async function () { + const usdnAmount = 10e8; + const usdtAmount = 10e8; + const shouldAutoStake = false; + const user1Address = address(this.accounts.user1, chainId); + + const expected1 = { + address: address(this.accounts.user1, chainId), + amount: 29996411, + asset: this.usdnAssetId, + }; + + const lpStable = address(this.accounts.lpStable, chainId); + const swapAmount = 30e6; + const feePoolAmount = 3e6; + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const poolUsdtBalanceBeforeInvoke = ( + await api.assets.fetchBalanceAddressAssetId( + user1Address, + this.usdnAssetId, + ) + ).balance; + + const invokeTx = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: swapAmount }, + ], + call: { + function: 'calculateAmountOutForSwapAndSendTokens', + args: [ + { type: 'integer', value: swapAmount }, + { type: 'boolean', value: false }, + { type: 'integer', value: 0 }, + { type: 'string', value: address(this.accounts.user1, chainId) }, + { type: 'integer', value: feePoolAmount }, + ], + }, + chainId, + }, this.accounts.swap); + await api.transactions.broadcast(invokeTx, {}); + await ni.waitForTx(invokeTx.id, { apiBase }); + + const poolUsdtBalanceAfterInvoke = ( + await api.assets.fetchBalanceAddressAssetId( + user1Address, + this.usdnAssetId, + ) + ).balance; + + expect(Number(poolUsdtBalanceAfterInvoke)) + .to.be.eql(Number(poolUsdtBalanceBeforeInvoke) + expected1.amount); + }); +}); diff --git a/test/components/lp_stable/calculateAmountOutForSwapREADONLY.mjs b/test/components/lp_stable/calculateAmountOutForSwapREADONLY.mjs new file mode 100644 index 000000000..dcc071306 --- /dev/null +++ b/test/components/lp_stable/calculateAmountOutForSwapREADONLY.mjs @@ -0,0 +1,52 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; +const api = create(apiBase); + +describe('lp_stable: calculateAmountOutForSwapREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it('should successfully calculateAmountOutForSwapREADONLY', async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const expected1 = { type: 'Int', value: 99257041 }; + + const lpStable = address(this.accounts.lpStable, chainId); + const swapAmount = 123e6; + const feePoolAmount = 3e6; + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr = `calculateAmountOutForSwapREADONLY(${swapAmount}, false, ${feePoolAmount})`; /* eslint-disable-line */ + const response = await api.utils.fetchEvaluate(lpStable, expr); + console.log(response.result); + const checkData = response.result.value._2; /* eslint-disable-line */ + + expect(checkData).to.eql(expected1); /* eslint-disable-line */ + }); +}); diff --git a/test/components/lp_stable/contract/tools.mjs b/test/components/lp_stable/contract/tools.mjs new file mode 100644 index 000000000..60902c7c7 --- /dev/null +++ b/test/components/lp_stable/contract/tools.mjs @@ -0,0 +1,46 @@ +export function flattenInvokesList(stateChanges) { + let outArray = []; + + stateChanges.invokes.forEach((element) => { + outArray.push([element.dApp, element.call.function]); + outArray = outArray.concat(flattenInvokesList(element.stateChanges)); + }); + + return outArray; +} + +export function flattenTransfers(stateChanges) { + let outArray = []; + + stateChanges.invokes.forEach((element) => { + outArray = outArray.concat(flattenTransfers(element.stateChanges)); + }); + + outArray = outArray.concat(stateChanges.transfers); + + return outArray; +} + +export function flattenInvokes(stateChanges) { + let outArray = []; + + stateChanges.invokes.forEach((element) => { + outArray = outArray.concat(flattenInvokes(element.stateChanges)); + }); + + outArray = outArray.concat(stateChanges.invokes); + + return outArray; +} + +export function flattenData(stateChanges) { + let outArray = []; + + stateChanges.invokes.forEach((element) => { + outArray = outArray.concat(flattenData(element.stateChanges)); + }); + + outArray = outArray.concat(stateChanges.data); + + return outArray; +} diff --git a/test/components/lp_stable/estimatePutOperationWrapperREADONLY.mjs b/test/components/lp_stable/estimatePutOperationWrapperREADONLY.mjs index ef23db8be..39cf6bc0b 100644 --- a/test/components/lp_stable/estimatePutOperationWrapperREADONLY.mjs +++ b/test/components/lp_stable/estimatePutOperationWrapperREADONLY.mjs @@ -34,17 +34,8 @@ describe( const expected7 = { type: 'ByteVector', value: this.lpStableAssetId }; const expected8 = { type: 'String', value: '1' }; const expected9 = { - type: 'IntegerEntry', - value: { - key: { - type: 'String', - value: '%s%s__price__last', - }, - value: { - type: 'Int', - value: 100000000, - }, - }, + type: 'Array', + value: [], }; const expected10 = { type: 'Int', value: 0 }; @@ -66,7 +57,7 @@ describe( expect(checkData._6).to.eql(expected6); /* eslint-disable-line */ expect(checkData._7).to.eql(expected7); /* eslint-disable-line */ expect(checkData._8).to.eql(expected8); /* eslint-disable-line */ - expect(checkData._9.value[0]).to.eql(expected9); /* eslint-disable-line */ + expect(checkData._9).to.eql(expected9); /* eslint-disable-line */ expect(checkData._10).to.eql(expected10); /* eslint-disable-line */ expect(checkData._11).to.eql(expected11); /* eslint-disable-line */ expect(checkData._12).to.eql(expected12); /* eslint-disable-line */ diff --git a/test/components/lp_stable/fromX18WrapperREADONLY.mjs b/test/components/lp_stable/fromX18WrapperREADONLY.mjs new file mode 100644 index 000000000..511ca6163 --- /dev/null +++ b/test/components/lp_stable/fromX18WrapperREADONLY.mjs @@ -0,0 +1,56 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: fromX18WrapperREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully fromX18WrapperREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr1 = `fromX18WrapperREADONLY("1234567890123456789012", 100000000)`; /* eslint-disable-line */ + const response1 = await api.utils.fetchEvaluate( + lpStable, + expr1, + ); + const evaluateData1 = response1.result.value._2; /* eslint-disable-line */ + + expect(evaluateData1).to.eql({ + type: 'Int', + value: 123456789012, + }); + }, + ); +}); diff --git a/test/components/lp_stable/get.mjs b/test/components/lp_stable/get.mjs index 62d88f5cd..03e1d8f90 100644 --- a/test/components/lp_stable/get.mjs +++ b/test/components/lp_stable/get.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -59,8 +60,9 @@ describe('lp_stable: get.mjs', /** @this {MochaSuiteModified} */() => { const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: `%s%s%s__G__${address(this.accounts.user1, chainId)}__${id}`, type: 'string', value: `%d%d%d%d%d%d__${usdtAmount / 10}__${usdnAmount / 10}__${lpStableAmount}__${priceLast}__${height}__${timestamp}`, @@ -82,7 +84,7 @@ describe('lp_stable: get.mjs', /** @this {MochaSuiteModified} */() => { value: '10000000000000006424805538327', }]); - expect(stateChanges.transfers).to.eql([{ + expect(flattenTransfers(stateChanges)).to.eql([{ address: address(this.accounts.user1, chainId), asset: this.usdtAssetId, amount: usdtAmount / 10, @@ -92,7 +94,7 @@ describe('lp_stable: get.mjs', /** @this {MochaSuiteModified} */() => { amount: (usdnAmount / 10).toString(), }]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'burn'], ]); diff --git a/test/components/lp_stable/getAccBalanceWrapperREADONLY.mjs b/test/components/lp_stable/getAccBalanceWrapperREADONLY.mjs new file mode 100644 index 000000000..88adfdb87 --- /dev/null +++ b/test/components/lp_stable/getAccBalanceWrapperREADONLY.mjs @@ -0,0 +1,68 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: getAccBalanceWrapperREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully getAccBalanceWrapperREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr1 = `getAccBalanceWrapperREADONLY("${this.usdtAssetId}")`; /* eslint-disable-line */ + const response1 = await api.utils.fetchEvaluate( + lpStable, + expr1, + ); + const evaluateData1 = response1.result.value._2; /* eslint-disable-line */ + + expect(evaluateData1).to.eql({ + type: 'Int', + value: usdtAmount, + }); + + const expr2 = `getAccBalanceWrapperREADONLY("${this.usdnAssetId}")`; /* eslint-disable-line */ + const response2 = await api.utils.fetchEvaluate( + lpStable, + expr2, + ); + const evaluateData = response2.result.value._2; /* eslint-disable-line */ + + expect(evaluateData).to.eql({ + type: 'Int', + value: usdnAmount, + }); + }, + ); +}); diff --git a/test/components/lp_stable/getManualUnstake.mjs b/test/components/lp_stable/getManualUnstake.mjs index 6768ba02f..2da1339ae 100644 --- a/test/components/lp_stable/getManualUnstake.mjs +++ b/test/components/lp_stable/getManualUnstake.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -73,8 +74,9 @@ describe('lp_stable: getManualUnstake.mjs', /** @this {MochaSuiteModified} */() const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: `%s%s%s__G__${address(this.accounts.user1, chainId)}__${id}`, type: 'string', value: `%d%d%d%d%d%d__${usdtAmount / 10}__${usdnAmount / 10}__${lpStableAmount}__${priceLast}__${height}__${timestamp}`, @@ -96,7 +98,7 @@ describe('lp_stable: getManualUnstake.mjs', /** @this {MochaSuiteModified} */() value: '10000000000000006424805538327', }]); - expect(stateChanges.transfers).to.eql([{ + expect(flattenTransfers(stateChanges)).to.eql([{ address: address(this.accounts.user1, chainId), asset: this.usdtAssetId, amount: usdtAmount / 10, @@ -106,7 +108,7 @@ describe('lp_stable: getManualUnstake.mjs', /** @this {MochaSuiteModified} */() amount: (usdnAmount / 10).toString(), }]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'burn'], ]); diff --git a/test/components/lp_stable/getNoLess.mjs b/test/components/lp_stable/getNoLess.mjs index be7852bf0..4104515cc 100644 --- a/test/components/lp_stable/getNoLess.mjs +++ b/test/components/lp_stable/getNoLess.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -65,8 +66,9 @@ describe('lp_stable: getNoLess.mjs', /** @this {MochaSuiteModified} */() => { const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: `%s%s%s__G__${address(this.accounts.user1, chainId)}__${id}`, type: 'string', value: `%d%d%d%d%d%d__${usdtAmount / 10}__${usdnAmount / 10}__${lpStableAmount}__${expectedPriceLast}__${height}__${timestamp}`, @@ -88,7 +90,7 @@ describe('lp_stable: getNoLess.mjs', /** @this {MochaSuiteModified} */() => { value: '10000000000000006424805538327', }]); - expect(stateChanges.transfers).to.eql([{ + expect(flattenTransfers(stateChanges)).to.eql([{ address: address(this.accounts.user1, chainId), asset: this.usdtAssetId, amount: usdtAmount / 10, @@ -98,7 +100,7 @@ describe('lp_stable: getNoLess.mjs', /** @this {MochaSuiteModified} */() => { amount: (usdnAmount / 10).toString(), }]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'burn'], ]); diff --git a/test/components/lp_stable/getOneTkn.mjs b/test/components/lp_stable/getOneTkn.mjs index 09850a419..bb2de4c34 100644 --- a/test/components/lp_stable/getOneTkn.mjs +++ b/test/components/lp_stable/getOneTkn.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -89,8 +90,9 @@ describe('lp_stable: getOneTkn.mjs', /** @this {MochaSuiteModified} */() => { const { timestamp } = await api.blocks.fetchHeadersAt(heightAfterGetOneTkn); const keyPriceHistory = `%s%s%d%d__price__history__${heightAfterGetOneTkn}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: `%s%s%s__G__${address(this.accounts.user1, chainId)}__${id}`, type: 'string', value: `%d%d%d%d%d%d__${expectedOutAmAmt}__${expectedOutPrAmt}__${outLp}__${expectedPriceLast}__${heightAfterGetOneTkn}__${timestamp}`, @@ -112,7 +114,7 @@ describe('lp_stable: getOneTkn.mjs', /** @this {MochaSuiteModified} */() => { value: '10000000127432425026570490178', }]); - expect(stateChanges.transfers).to.eql([ + expect(flattenTransfers(stateChanges)).to.deep.include.members([ { address: address(this.accounts.user1, chainId), asset: this.usdtAssetId, @@ -125,7 +127,7 @@ describe('lp_stable: getOneTkn.mjs', /** @this {MochaSuiteModified} */() => { }, ]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'burn'], ]); diff --git a/test/components/lp_stable/getOneTknV2READONLY.mjs b/test/components/lp_stable/getOneTknV2READONLY.mjs new file mode 100644 index 000000000..27d48fd17 --- /dev/null +++ b/test/components/lp_stable/getOneTknV2READONLY.mjs @@ -0,0 +1,66 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: getOneTknV2READONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully getOneTknV2READONLY', + async function () { + const usdnAmount = 1e16 / 10; + const usdtAmount = 1e8 / 10; + const shouldAutoStake = false; + const lpStableAmount = 200000000 * 10e8; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr = `getOneTknV2READONLY("${this.usdtAssetId}", ${lpStableAmount})`; /* eslint-disable-line */ + const response = await api.utils.fetchEvaluate( + lpStable, + expr, + ); + const evaluateData = response.result.value._2; /* eslint-disable-line */ + + expect(evaluateData).to.eql({ + type: 'Tuple', + value: { + _1: { + type: 'Int', + value: '2964892755865620', + }, + _2: { + type: 'Int', + value: 2967860616482, + }, + }, + }); + }, + ); +}); diff --git a/test/components/lp_stable/getOneTknV2WithBonusREADONLY.mjs b/test/components/lp_stable/getOneTknV2WithBonusREADONLY.mjs new file mode 100644 index 000000000..93a6fcc98 --- /dev/null +++ b/test/components/lp_stable/getOneTknV2WithBonusREADONLY.mjs @@ -0,0 +1,70 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: getOneTknV2WithBonusREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully getOneTknV2WithBonusREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + const lpStableAmount = 10000000; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr = `getOneTknV2WithBonusREADONLY("${this.usdtAssetId}", ${lpStableAmount})`; /* eslint-disable-line */ + const response = await api.utils.fetchEvaluate( + lpStable, + expr, + ); + const evaluateData = response.result.value._2; /* eslint-disable-line */ + + expect(evaluateData).to.eql({ + type: 'Tuple', + value: { + _1: { + type: 'Int', + value: 99900, + }, + _2: { + type: 'Int', + value: 99, + }, + _3: { + type: 'Int', + value: -100000, + }, + }, + }); + }, + ); +}); diff --git a/test/components/lp_stable/getPoolConfigWrapperREADONLY.mjs b/test/components/lp_stable/getPoolConfigWrapperREADONLY.mjs new file mode 100644 index 000000000..c35b0d99d --- /dev/null +++ b/test/components/lp_stable/getPoolConfigWrapperREADONLY.mjs @@ -0,0 +1,78 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: getPoolConfigWrapperREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully getPoolConfigWrapperREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr = `getPoolConfigWrapperREADONLY()`; /* eslint-disable-line */ + const response = await api.utils.fetchEvaluate( + lpStable, + expr, + ); + const evaluateData = response.result.value._2; /* eslint-disable-line */ + + expect(evaluateData).to.eql({ + type: 'Array', + value: [ + { type: 'String', value: '%s%d%s%s%s%d%d%d%d%d%s' }, + { type: 'String', value: lpStable }, + { type: 'String', value: '1' }, + { + type: 'String', + value: this.lpStableAssetId, + }, + { + type: 'String', + value: this.usdtAssetId, + }, + { + type: 'String', + value: this.usdnAssetId, + }, + { type: 'String', value: '1000000' }, + { type: 'String', value: '1000000' }, + { type: 'String', value: '1' }, + { type: 'String', value: '2' }, + { type: 'String', value: '100000000' }, + { type: 'String', value: '' }, + ], + }); + }, + ); +}); diff --git a/test/components/lp_stable/mock/swap.mock.ride b/test/components/lp_stable/mock/swap.mock.ride new file mode 100644 index 000000000..666ea53be --- /dev/null +++ b/test/components/lp_stable/mock/swap.mock.ride @@ -0,0 +1,8 @@ +{-# STDLIB_VERSION 6 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +@Callable(i) +func isPoolSwapDisabledREADONLY(poolAddress: String) = { + (nil, false) +} diff --git a/test/components/lp_stable/put.mjs b/test/components/lp_stable/put.mjs index 2de8df8d4..e485db04b 100644 --- a/test/components/lp_stable/put.mjs +++ b/test/components/lp_stable/put.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -23,6 +24,9 @@ describe('lp_stable: put.mjs', /** @this {MochaSuiteModified} */() => { // TODO calculate dLp const lpStable = address(this.accounts.lpStable, chainId); + const userAddress = address(this.accounts.user1, chainId); + const userBeforeBalance = await api.assets + .fetchBalanceAddressAssetId(userAddress, this.lpStableAssetId); const put = invokeScript({ dApp: lpStable, @@ -45,7 +49,11 @@ describe('lp_stable: put.mjs', /** @this {MochaSuiteModified} */() => { const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; - expect(stateChanges.data).to.eql([{ + const lpStableState = await api.addresses.data(lpStable); + const userAfterBalance = await api.assets + .fetchBalanceAddressAssetId(userAddress, this.lpStableAssetId); + + expect(lpStableState).to.include.deep.members([{ key: '%s%s__price__last', type: 'integer', value: priceLast.toString(), @@ -67,13 +75,10 @@ describe('lp_stable: put.mjs', /** @this {MochaSuiteModified} */() => { value: '10000000000000003120271887017', }]); - expect(stateChanges.transfers).to.eql([{ - address: address(this.accounts.user1, chainId), - asset: this.lpStableAssetId, - amount: expectedLpAmount.toString(), - }]); + expect(Number(userAfterBalance.balance)) + .to.eql(Number(userBeforeBalance.balance) + expectedLpAmount); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'emit'], ]); diff --git a/test/components/lp_stable/putAutoStake.mjs b/test/components/lp_stable/putAutoStake.mjs index 4469198f2..ad0375a1f 100644 --- a/test/components/lp_stable/putAutoStake.mjs +++ b/test/components/lp_stable/putAutoStake.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -43,8 +44,9 @@ describe('lp_stable: putAutoStake.mjs', /** @this {MochaSuiteModified} */() => { const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: '%s%s__price__last', type: 'integer', value: priceLast.toString(), @@ -66,7 +68,7 @@ describe('lp_stable: putAutoStake.mjs', /** @this {MochaSuiteModified} */() => { value: '10000000000000003120271887017', }]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'emit'], [address(this.accounts.staking, chainId), 'stake'], diff --git a/test/components/lp_stable/putForFree.mjs b/test/components/lp_stable/putForFree.mjs new file mode 100644 index 000000000..784779e07 --- /dev/null +++ b/test/components/lp_stable/putForFree.mjs @@ -0,0 +1,103 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; +import { flattenInvokesList } from './contract/tools.mjs'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: putForFree.mjs', /** @this {MochaSuiteModified} */() => { + it('should successfully put without lp emission', async function () { + const usdnAmount = 1e16 / 10; + const usdtAmount = 1e8 / 10; + const expectedLpAmount = 0; + const priceLast = 1e16; + const priceHistory = 1e16; + // TODO calculate dLp + + const lpStable = address(this.accounts.lpStable, chainId); + const userAddress = address(this.accounts.user1, chainId); + + const initPut = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: false }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(initPut, {}); + await ni.waitForTx(initPut.id, { apiBase }); + + const userBeforeBalance = await api.assets + .fetchBalanceAddressAssetId(userAddress, this.lpStableAssetId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'putForFree', + args: [ + { type: 'integer', value: 1000000 }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + const { height, stateChanges, id } = await ni.waitForTx(put.id, { apiBase }); + + const { timestamp } = await api.blocks.fetchHeadersAt(height); + const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + + const lpStableState = await api.addresses.data(lpStable); + const userAfterBalance = await api.assets + .fetchBalanceAddressAssetId(userAddress, this.lpStableAssetId); + + expect(lpStableState).to.include.deep.members([{ + key: '%s%s__price__last', + type: 'integer', + value: priceLast.toString(), + }, { + key: keyPriceHistory, + type: 'integer', + value: priceHistory.toString(), + }, { + key: `%s%s%s__P__${address(this.accounts.user1, chainId)}__${id}`, + type: 'string', + value: `%d%d%d%d%d%d%d%d%d%d__${usdtAmount}__${usdnAmount}__${expectedLpAmount}__${priceLast}__1000000__1000000__${height}__${timestamp}__0__0`, + }, { + key: '%s__dLpRefreshedHeight', + type: 'integer', + value: height, + }, { + key: '%s__dLp', + type: 'string', + value: '20000000000000006240543774034', + }]); + + expect(Number(userAfterBalance.balance)) + .to.eql(Number(userBeforeBalance.balance) + expectedLpAmount); + + expect(flattenInvokesList(stateChanges)) + .to.deep.not.include.members([ + [address(this.accounts.factoryV2, chainId), 'emit'], + ]); + }); +}); diff --git a/test/components/lp_stable/putOneTkn.mjs b/test/components/lp_stable/putOneTkn.mjs index 45459973c..6bf238a11 100644 --- a/test/components/lp_stable/putOneTkn.mjs +++ b/test/components/lp_stable/putOneTkn.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -26,7 +27,7 @@ describe('lp_stable: putOneTkn.mjs', /** @this {MochaSuiteModified} */() => { const expectedWritePrAmt = 0; const expectedEmitLpAmt = 9982549115; const expectedFee = 100000; - const expectedslippageCalc = 0; + const expectedSlippageCalc = 0; const expectedAmDiff = 0; const expectedPrDiff = 0; @@ -69,8 +70,9 @@ describe('lp_stable: putOneTkn.mjs', /** @this {MochaSuiteModified} */() => { const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: '%s%s__price__last', type: 'integer', value: expectedPriceLast, @@ -81,7 +83,7 @@ describe('lp_stable: putOneTkn.mjs', /** @this {MochaSuiteModified} */() => { }, { key: `%s%s%s__P__${address(this.accounts.user1, chainId)}__${id}`, type: 'string', - value: `%d%d%d%d%d%d%d%d%d%d__${expectedWriteAmAmt}__${expectedWritePrAmt}__${expectedEmitLpAmt}__${expectedPriceLast}__${slippage}__${expectedslippageCalc}__${height}__${timestamp}__${expectedAmDiff}__${expectedPrDiff}`, + value: `%d%d%d%d%d%d%d%d%d%d__${expectedWriteAmAmt}__${expectedWritePrAmt}__${expectedEmitLpAmt}__${expectedPriceLast}__${slippage}__${expectedSlippageCalc}__${height}__${timestamp}__${expectedAmDiff}__${expectedPrDiff}`, }, { key: '%s__dLpRefreshedHeight', type: 'integer', @@ -92,7 +94,7 @@ describe('lp_stable: putOneTkn.mjs', /** @this {MochaSuiteModified} */() => { value: '10000000000068219985437352296', }]); - expect(stateChanges.transfers).to.eql([ + expect(flattenTransfers(stateChanges)).to.include.deep.members([ { address: address(this.accounts.user1, chainId), asset: this.lpStableAssetId, @@ -105,7 +107,7 @@ describe('lp_stable: putOneTkn.mjs', /** @this {MochaSuiteModified} */() => { }, ]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'emit'], ]); diff --git a/test/components/lp_stable/putOneTknAutoStake.mjs b/test/components/lp_stable/putOneTknAutoStake.mjs index f5ff9d2c3..de3a68990 100644 --- a/test/components/lp_stable/putOneTknAutoStake.mjs +++ b/test/components/lp_stable/putOneTknAutoStake.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -25,7 +26,7 @@ describe('lp_stable: putOneTknAutoStake.mjs', /** @this {MochaSuiteModified} */( const expectedWriteAmAmt = 1e8; const expectedWritePrAmt = 0; const expectedEmitLpAmt = 9982549115; - const expectedslippageCalc = 0; + const expectedSlippageCalc = 0; const expectedAmDiff = 0; const expectedPrDiff = 0; @@ -68,8 +69,9 @@ describe('lp_stable: putOneTknAutoStake.mjs', /** @this {MochaSuiteModified} */( const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: '%s%s__price__last', type: 'integer', value: expectedPriceLast, @@ -80,7 +82,7 @@ describe('lp_stable: putOneTknAutoStake.mjs', /** @this {MochaSuiteModified} */( }, { key: `%s%s%s__P__${address(this.accounts.user1, chainId)}__${id}`, type: 'string', - value: `%d%d%d%d%d%d%d%d%d%d__${expectedWriteAmAmt}__${expectedWritePrAmt}__${expectedEmitLpAmt}__${expectedPriceLast}__${slippage}__${expectedslippageCalc}__${height}__${timestamp}__${expectedAmDiff}__${expectedPrDiff}`, + value: `%d%d%d%d%d%d%d%d%d%d__${expectedWriteAmAmt}__${expectedWritePrAmt}__${expectedEmitLpAmt}__${expectedPriceLast}__${slippage}__${expectedSlippageCalc}__${height}__${timestamp}__${expectedAmDiff}__${expectedPrDiff}`, }, { key: '%s__dLpRefreshedHeight', type: 'integer', @@ -91,7 +93,7 @@ describe('lp_stable: putOneTknAutoStake.mjs', /** @this {MochaSuiteModified} */( value: '10000000000068219985437352296', }]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'emit'], [address(this.accounts.staking, chainId), 'stakeFor'], diff --git a/test/components/lp_stable/putOneTknV2WithBonusREADONLY.mjs b/test/components/lp_stable/putOneTknV2WithBonusREADONLY.mjs new file mode 100644 index 000000000..aa5623209 --- /dev/null +++ b/test/components/lp_stable/putOneTknV2WithBonusREADONLY.mjs @@ -0,0 +1,69 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: putOneTknV2WithBonusREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully putOneTknV2WithBonusREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr = `putOneTknV2WithBonusREADONLY(${usdtAmount}, "${this.usdtAssetId}")`; /* eslint-disable-line */ + const response = await api.utils.fetchEvaluate( + lpStable, + expr, + ); + const evaluateData = response.result.value._2; /* eslint-disable-line */ + + expect(evaluateData).to.eql({ + type: 'Tuple', + value: { + _1: { + type: 'Int', + value: 9982549115, + }, + _2: { + type: 'Int', + value: 100000, + }, + _3: { + type: 'Int', + value: -174508, + }, + }, + }); + }, + ); +}); diff --git a/test/components/lp_stable/putOneTknV2WithoutTakeFeeREADONLY.mjs b/test/components/lp_stable/putOneTknV2WithoutTakeFeeREADONLY.mjs new file mode 100644 index 000000000..a72b26cb8 --- /dev/null +++ b/test/components/lp_stable/putOneTknV2WithoutTakeFeeREADONLY.mjs @@ -0,0 +1,69 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: putOneTknV2WithoutTakeFeeREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully putOneTknV2WithoutTakeFeeREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr = `putOneTknV2WithoutTakeFeeREADONLY(${usdtAmount / 2}, "${this.usdtAssetId}")`; /* eslint-disable-line */ + const response = await api.utils.fetchEvaluate( + lpStable, + expr, + ); + const evaluateData = response.result.value._2; /* eslint-disable-line */ + + expect(evaluateData).to.eql({ + type: 'Tuple', + value: { + _1: { + type: 'Int', + value: 4997925482, + }, + _2: { + type: 'Int', + value: 0, + }, + _3: { + type: 'Int', + value: -41490, + }, + }, + }); + }, + ); +}); diff --git a/test/components/lp_stable/putTestnetStand.mjs b/test/components/lp_stable/putTestnetStand.mjs index fce8f51f0..015f61ef0 100644 --- a/test/components/lp_stable/putTestnetStand.mjs +++ b/test/components/lp_stable/putTestnetStand.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address, publicKey } from '@waves/ts-lib-crypto'; import { transfer, invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -17,6 +18,7 @@ describe('lp_stable: putTestnetStand.mjs', /** @this {MochaSuiteModified} */() = it('should successfully put with shouldAutoStake false in testnet stand', async function () { const rest = address(this.accounts.rest, chainId); const lpStable = address(this.accounts.lpStable, chainId); + const userAddress = address(this.accounts.user1, chainId); const transferAmountUsdn = 139018444021; const transferAmountUsdt = 230660086797; @@ -88,6 +90,8 @@ describe('lp_stable: putTestnetStand.mjs', /** @this {MochaSuiteModified} */() = const expectedPoolUsdtBalanceAfterPut = transferAmountUsdt + usdtAmount; const expectedPoolUsdnBalanceAfterPut = transferAmountUsdn + usdnAmount; + const userBeforeBalance = await api.assets + .fetchBalanceAddressAssetId(userAddress, this.lpStableAssetId); const put = invokeScript({ dApp: lpStable, @@ -131,8 +135,11 @@ describe('lp_stable: putTestnetStand.mjs', /** @this {MochaSuiteModified} */() = const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); + const userAfterBalance = await api.assets + .fetchBalanceAddressAssetId(userAddress, this.lpStableAssetId); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: '%s%s__price__last', type: 'integer', value: priceLast, @@ -154,13 +161,10 @@ describe('lp_stable: putTestnetStand.mjs', /** @this {MochaSuiteModified} */() = value: '29940057202414181477464152049', }]); - expect(stateChanges.transfers).to.eql([{ - address: address(this.accounts.user1, chainId), - asset: this.lpStableAssetId, - amount: expectedLpAmount, - }]); + expect(Number(userAfterBalance.balance)) + .to.eql(Number(userBeforeBalance.balance) + expectedLpAmount); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'emit'], ]); diff --git a/test/components/lp_stable/put_one_tkn_get_one_tkn.mjs b/test/components/lp_stable/put_one_tkn_get_one_tkn.mjs index a9f1a18c3..f4c1e50b5 100644 --- a/test/components/lp_stable/put_one_tkn_get_one_tkn.mjs +++ b/test/components/lp_stable/put_one_tkn_get_one_tkn.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address, publicKey } from '@waves/ts-lib-crypto'; import { lpStable } from './contract/lp_stable.mjs'; import { chainId } from '../../utils/api.mjs'; +import { flattenTransfers } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -45,7 +46,7 @@ describe('lp_stable: put_one_tkn_get_one_tkn.mjs put one token, get one token', let lpAssetAmount; { - const transfersToUser = putOneTknInfo.stateChanges.transfers + const transfersToUser = flattenTransfers(putOneTknInfo.stateChanges) .filter((t) => t.address === address(caller, chainId)); expect(transfersToUser.length).to.equal(1, '1 transfer to caller is expected'); @@ -66,7 +67,7 @@ describe('lp_stable: put_one_tkn_get_one_tkn.mjs put one token, get one token', let outAssetAmount; { - const transfersToUser = getOneTknInfo.stateChanges.transfers + const transfersToUser = flattenTransfers(getOneTknInfo.stateChanges) .filter((t) => t.address === address(caller, chainId)); expect(transfersToUser.length).to.equal(1, '1 transfer to caller is expected'); diff --git a/test/components/lp_stable/put_one_tkn_unstake_and_get_one_tkn.mjs b/test/components/lp_stable/put_one_tkn_unstake_and_get_one_tkn.mjs index a55d36c43..f8b9251d8 100644 --- a/test/components/lp_stable/put_one_tkn_unstake_and_get_one_tkn.mjs +++ b/test/components/lp_stable/put_one_tkn_unstake_and_get_one_tkn.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address, publicKey } from '@waves/ts-lib-crypto'; import { lpStable } from './contract/lp_stable.mjs'; import { chainId } from '../../utils/api.mjs'; +import { flattenTransfers, flattenInvokes } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -15,7 +16,7 @@ describe('lp_stable: put_one_tkn_unstake_and_get_one_tkn.mjs put one token, get value: 0, }); }); - it('user should reveive the same amount after get minus fees', async function () { + it('user should receive the same amount after get minus fees', async function () { const dApp = address(this.accounts.lpStable, chainId); const caller = this.accounts.user1; const amountAssetId = this.usdtAssetId; @@ -46,12 +47,12 @@ describe('lp_stable: put_one_tkn_unstake_and_get_one_tkn.mjs put one token, get let lpAssetAmount; { - const transfersToUser = putOneTknInfo.stateChanges.transfers + const transfersToUser = flattenTransfers(putOneTknInfo.stateChanges) .filter((t) => t.address === address(caller, chainId)); expect(transfersToUser.length).to.equal(0, 'no transfers to caller are expected'); - const stakingInvokes = putOneTknInfo.stateChanges.invokes + const stakingInvokes = flattenInvokes(putOneTknInfo.stateChanges) .filter((t) => t.dApp === address(this.accounts.staking, chainId)); expect(stakingInvokes.length).to.equal(1, '1 staking invoke is expected'); @@ -72,7 +73,7 @@ describe('lp_stable: put_one_tkn_unstake_and_get_one_tkn.mjs put one token, get let outAssetAmount; { - const transfersToUser = unstakeAndGetOneTknInfo.stateChanges.transfers + const transfersToUser = flattenTransfers(unstakeAndGetOneTknInfo.stateChanges) .filter((t) => t.address === address(caller, chainId)); expect(transfersToUser.length).to.equal(1, '1 transfer to caller is expected'); diff --git a/test/components/lp_stable/refreshDLp.mjs b/test/components/lp_stable/refreshDLp.mjs new file mode 100644 index 000000000..a23c9bcd7 --- /dev/null +++ b/test/components/lp_stable/refreshDLp.mjs @@ -0,0 +1,90 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni, transfer } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: refreshDLp.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully refreshDLp', + async function () { + const usdnAmount = 1e16 / 10; + const usdtAmount = 1e8 / 10; + const shouldAutoStake = false; + const delay = 2; + const expectedDLp = '12569372929872107410759846168'; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + const { height } = await ni.waitForTx(put.id, { apiBase }); + + await api.transactions.broadcast(transfer({ + amount: usdtAmount, + assetId: this.usdtAssetId, + recipient: lpStable, + }, this.accounts.user1), {}); + + await ni.waitForHeight(height + delay, { apiBase }); + + const expr = `refreshDLp()`; /* eslint-disable-line */ + const response = await api.utils.fetchEvaluate( + lpStable, + expr, + ); + const evaluateData = response.result.value._2; /* eslint-disable-line */ + + const refreshDLpInvoke = invokeScript({ + dApp: lpStable, + payment: [], + call: { + function: 'refreshDLp', + args: [], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(refreshDLpInvoke, {}); + const { height: refreshHeight } = await ni.waitForTx(refreshDLpInvoke.id, { apiBase }); + + const lpStableState = await api.addresses.data(lpStable); + + expect(evaluateData).to.eql({ + type: 'String', + value: expectedDLp, + }); + + expect(lpStableState).to.include.deep.members([{ + key: '%s__dLpRefreshedHeight', + type: 'integer', + value: refreshHeight, + }, { + key: '%s__dLp', + type: 'string', + value: expectedDLp, + }]); + }, + ); +}); diff --git a/test/components/lp_stable/toX18WrapperREADONLY.mjs b/test/components/lp_stable/toX18WrapperREADONLY.mjs new file mode 100644 index 000000000..2222273fd --- /dev/null +++ b/test/components/lp_stable/toX18WrapperREADONLY.mjs @@ -0,0 +1,56 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: toX18WrapperREADONLY.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully toX18WrapperREADONLY', + async function () { + const usdnAmount = 1e8; + const usdtAmount = 1e8; + const shouldAutoStake = false; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const expr1 = `toX18WrapperREADONLY(123456, 100000000)`; /* eslint-disable-line */ + const response1 = await api.utils.fetchEvaluate( + lpStable, + expr1, + ); + const evaluateData1 = response1.result.value._2; /* eslint-disable-line */ + + expect(evaluateData1).to.eql({ + type: 'String', + value: '1234560000000000', + }); + }, + ); +}); diff --git a/test/components/lp_stable/unstakeAndGet.mjs b/test/components/lp_stable/unstakeAndGet.mjs index dbf9999cc..21908353b 100644 --- a/test/components/lp_stable/unstakeAndGet.mjs +++ b/test/components/lp_stable/unstakeAndGet.mjs @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised'; import { address } from '@waves/ts-lib-crypto'; import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; chai.use(chaiAsPromised); const { expect } = chai; @@ -58,8 +59,9 @@ describe('lp_stable: unstakeAndGet.mjs', /** @this {MochaSuiteModified} */() => const { timestamp } = await api.blocks.fetchHeadersAt(height); const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); - expect(stateChanges.data).to.eql([{ + expect(lpStableState).to.include.deep.members([{ key: `%s%s%s__G__${address(this.accounts.user1, chainId)}__${id}`, type: 'string', value: `%d%d%d%d%d%d__${usdtAmount / 10}__${usdnAmount / 10}__${lpStableAmount}__${priceLast}__${height}__${timestamp}`, @@ -81,7 +83,7 @@ describe('lp_stable: unstakeAndGet.mjs', /** @this {MochaSuiteModified} */() => value: '10000000000000006424805538327', }]); - expect(stateChanges.transfers).to.eql([{ + expect(flattenTransfers(stateChanges)).to.deep.include.members([{ address: address(this.accounts.user1, chainId), asset: this.usdtAssetId, amount: usdtAmount / 10, @@ -91,7 +93,7 @@ describe('lp_stable: unstakeAndGet.mjs', /** @this {MochaSuiteModified} */() => amount: (usdnAmount / 10).toString(), }]); - expect(stateChanges.invokes.map((item) => [item.dApp, item.call.function])) + expect(flattenInvokesList(stateChanges)) .to.deep.include.members([ [address(this.accounts.factoryV2, chainId), 'burn'], ]); diff --git a/test/components/lp_stable/unstakeAndGetNoLess.mjs b/test/components/lp_stable/unstakeAndGetNoLess.mjs new file mode 100644 index 000000000..12513279e --- /dev/null +++ b/test/components/lp_stable/unstakeAndGetNoLess.mjs @@ -0,0 +1,104 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: unstakeAndGetNoLess.mjs', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully unstakeAndGetNoLess. The put method uses the shouldAutoStake argument with a value of true', + async function () { + const usdnAmount = 1e16 / 10; + const usdtAmount = 1e8 / 10; + const lpStableAmount = 268990720838218; + const shouldAutoStake = true; + const priceLast = 1e16; + const priceHistory = 1e16; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: shouldAutoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const unstakeAndGet = invokeScript({ + dApp: lpStable, + call: { + function: 'unstakeAndGetNoLess', + args: [ + { type: 'integer', value: lpStableAmount }, + { type: 'integer', value: usdtAmount / 10 }, + { type: 'integer', value: usdnAmount / 10 }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(unstakeAndGet, {}); + const { height, stateChanges, id } = await ni.waitForTx(unstakeAndGet.id, { apiBase }); + + const { timestamp } = await api.blocks.fetchHeadersAt(height); + const keyPriceHistory = `%s%s%d%d__price__history__${height}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); + + expect(lpStableState).to.include.deep.members([{ + key: `%s%s%s__G__${address(this.accounts.user1, chainId)}__${id}`, + type: 'string', + value: `%d%d%d%d%d%d__${usdtAmount / 10}__${usdnAmount / 10}__${lpStableAmount}__${priceLast}__${height}__${timestamp}`, + }, { + key: '%s%s__price__last', + type: 'integer', + value: priceLast.toString(), + }, { + key: keyPriceHistory, + type: 'integer', + value: priceHistory.toString(), + }, { + key: '%s__dLpRefreshedHeight', + type: 'integer', + value: height, + }, { + key: '%s__dLp', + type: 'string', + value: '10000000000000006424805538327', + }]); + + expect(flattenTransfers(stateChanges)).to.deep.include.members([{ + address: address(this.accounts.user1, chainId), + asset: this.usdtAssetId, + amount: usdtAmount / 10, + }, { + address: address(this.accounts.user1, chainId), + asset: this.usdnAssetId, + amount: (usdnAmount / 10).toString(), + }]); + + expect(flattenInvokesList(stateChanges)) + .to.deep.include.members([ + [address(this.accounts.factoryV2, chainId), 'burn'], + ]); + }, + ); +}); diff --git a/test/components/lp_stable/unstakeAndgetOneTknV2.mjs b/test/components/lp_stable/unstakeAndgetOneTknV2.mjs new file mode 100644 index 000000000..426922f03 --- /dev/null +++ b/test/components/lp_stable/unstakeAndgetOneTknV2.mjs @@ -0,0 +1,134 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { address } from '@waves/ts-lib-crypto'; +import { invokeScript, nodeInteraction as ni } from '@waves/waves-transactions'; +import { create } from '@waves/node-api-js'; +import { flattenInvokesList, flattenTransfers } from './contract/tools.mjs'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const apiBase = process.env.API_NODE_URL; +const chainId = 'R'; + +const api = create(apiBase); + +describe('lp_stable: unstakeAndGetOneTknV2.mjs', /** @this {MochaSuiteModified} */() => { + it('should successfully unstakeAndGetOneTknV2.', async function () { + const outLp = 1e10; + const autoStake = true; + const usdtAmount = 1e8; + const usdnAmount = 1e8; + const minOutAmount = 0; + const delay = 2; + const expectedPriceLast = 100174811; + const expectedPriceHistory = 100174811; + + const expectedOutAmAmt = 99974432; + const expectedFee = 100074; + const expectedOutPrAmt = 0; + + const lpStable = address(this.accounts.lpStable, chainId); + + const put = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + { assetId: this.usdnAssetId, amount: usdnAmount }, + ], + call: { + function: 'put', + args: [ + { type: 'integer', value: 0 }, + { type: 'boolean', value: autoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(put, {}); + await ni.waitForTx(put.id, { apiBase }); + + const putOneTkn = invokeScript({ + dApp: lpStable, + payment: [ + { assetId: this.usdtAssetId, amount: usdtAmount }, + ], + call: { + function: 'putOneTknV2', + args: [ + { type: 'integer', value: minOutAmount }, + { type: 'boolean', value: autoStake }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(putOneTkn, {}); + const { height } = await ni.waitForTx(putOneTkn.id, { apiBase }); + + await ni.waitForHeight(height + delay, { apiBase }); + + const getOneTkn = invokeScript({ + dApp: lpStable, + payment: [], + call: { + function: 'unstakeAndGetOneTknV2', + args: [ + { type: 'integer', value: outLp }, + { type: 'string', value: this.usdtAssetId }, + { type: 'integer', value: minOutAmount }, + ], + }, + chainId, + }, this.accounts.user1); + await api.transactions.broadcast(getOneTkn, {}); + const { + height: heightAfterGetOneTkn, + stateChanges, + id, + } = await ni.waitForTx(getOneTkn.id, { apiBase }); + + const { timestamp } = await api.blocks.fetchHeadersAt(heightAfterGetOneTkn); + const keyPriceHistory = `%s%s%d%d__price__history__${heightAfterGetOneTkn}__${timestamp}`; + const lpStableState = await api.addresses.data(lpStable); + + expect(lpStableState).to.include.deep.members([{ + key: `%s%s%s__G__${address(this.accounts.user1, chainId)}__${id}`, + type: 'string', + value: `%d%d%d%d%d%d__${expectedOutAmAmt}__${expectedOutPrAmt}__${outLp}__${expectedPriceLast}__${heightAfterGetOneTkn}__${timestamp}`, + }, { + key: '%s%s__price__last', + type: 'integer', + value: expectedPriceLast, + }, { + key: keyPriceHistory, + type: 'integer', + value: expectedPriceHistory, + }, { + key: '%s__dLpRefreshedHeight', + type: 'integer', + value: heightAfterGetOneTkn, + }, { + key: '%s__dLp', + type: 'string', + value: '10000000127432425026570490178', + }]); + + expect(flattenTransfers(stateChanges)).to.deep.include.members([ + { + address: address(this.accounts.user1, chainId), + asset: this.usdtAssetId, + amount: expectedOutAmAmt, + }, + { + address: address(this.accounts.feeCollector, chainId), + asset: this.usdtAssetId, + amount: expectedFee, + }, + ]); + + expect(flattenInvokesList(stateChanges)) + .to.deep.include.members([ + [address(this.accounts.factoryV2, chainId), 'burn'], + ]); + }); +});