UNPKG

@chorus-one/ton

Version:

All-in-one tooling for building staking dApps on TON

629 lines (628 loc) 34.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TonPoolStaker = void 0; const ton_1 = require("@ton/ton"); const TonBaseStaker_1 = require("./TonBaseStaker"); const utils_1 = require("./utils"); class TonPoolStaker extends TonBaseStaker_1.TonBaseStaker { /** * Builds a staking transaction for TON Pool contract. It uses 2 pool solution, and picks the best pool * to stake to automatically. * * @param params - Parameters for building the transaction * @param params.delegatorAddress - The delegator address * @param params.validatorAddressPair - The validator address pair to stake to * @param params.amount - The amount to stake, specified in `TON` * @param params.preferredStrategy - (Optional) The stake allocation strategy. Default is `balanced`. * * `balanced` - automatically balances the stake between the two pools based on the current pool balances and user stakes * * `split` - splits the stake evenly between the two pools * * `single` - stakes to a single pool * @param params.referrer - (Optional) The address of the referrer. This is used to track the origin of transactions, * providing insights into which sources or campaigns are driving activity. This can be useful for analytics and * optimizing user acquisition strategies * @param params.validUntil - (Optional) The Unix timestamp when the transaction expires * * @returns Returns a promise that resolves to a TON nominator pool staking transaction. */ async buildStakeTx(params) { const { validatorAddressPair, delegatorAddress, amount, preferredStrategy, validUntil, referrer } = params; // allow staking to both pools const validatorAddresses = validatorAddressPair.filter((address) => address.length > 0); if (validatorAddresses.length == 0) { throw new Error('At least one validator address is required'); } validatorAddresses.forEach((validatorAddress) => { // ensure the address is for the right network this.checkIfAddressTestnetFlagMatches(validatorAddress); // ensure the validator address is bounceable. // NOTE: TEP-002 specifies that the address bounceable flag should match both the internal message and the address. // This has no effect as we force the bounce flag anyway. However it is a good practice to be consistent if (!ton_1.Address.parseFriendly(validatorAddress).isBounceable) { throw new Error('validator address is not bounceable! It is required for nominator pool contract operations to use bounceable addresses'); } }); const genStakeMsg = (validatorAddress, amount) => { // https://github.com/tonwhales/ton-nominators/blob/0553e1b6ddfc5c0b60505957505ce58d01bec3e7/compiled/nominators.fc#L18 let basePayload = (0, ton_1.beginCell)() .storeUint(2077040623, 32) // stake_deposit method const .storeUint((0, TonBaseStaker_1.getRandomQueryId)(), 64) // Query ID .storeCoins((0, TonBaseStaker_1.getDefaultGas)()); // Gas if (referrer) { basePayload = basePayload.storeStringTail(referrer); } const payload = basePayload.endCell(); return { address: validatorAddress, bounceable: true, amount: amount, payload }; }; const { minElectionStake, currentPoolBalances } = await this.getPoolDataForDelegator(delegatorAddress, validatorAddresses); const poolParams = []; for (const validatorAddress of validatorAddresses) { const params = await this.getPoolParamsUnformatted({ validatorAddress }); poolParams.push(params); } const lowestMinStake = poolParams .filter((param) => param.minStake !== 0n) .reduce((acc, val) => (val.minStakeTotal < acc ? val.minStakeTotal : acc), poolParams[0].minStakeTotal); if (lowestMinStake === 0n) { throw new Error('minimum stake for both pools is zero, that does not seem right'); } if ((0, ton_1.toNano)(amount) < lowestMinStake) { throw new Error('provided amount is less than the minimum required to stake'); } const selectedStrategy = TonPoolStaker.selectStrategy(preferredStrategy, (0, ton_1.toNano)(amount), validatorAddresses.length, lowestMinStake); const msgs = []; switch (selectedStrategy) { case 'single': { const poolIndex = TonPoolStaker.selectPool(minElectionStake, currentPoolBalances); msgs.push(genStakeMsg(validatorAddressPair[poolIndex], (0, ton_1.toNano)(amount))); break; } case 'split': { const amounts = [0n, 0n]; amounts[0] = (0, ton_1.toNano)(amount) / 2n; amounts[1] = (0, ton_1.toNano)(amount) - amounts[0]; msgs.push(genStakeMsg(validatorAddressPair[0], amounts[0])); msgs.push(genStakeMsg(validatorAddressPair[1], amounts[1])); break; } case 'balanced': { const stakeAmountPerPool = TonPoolStaker.calculateStakePoolAmount((0, ton_1.toNano)(amount), minElectionStake, currentPoolBalances, [poolParams[0].minStake, poolParams[1].minStake]); validatorAddresses.forEach((validatorAddress, index) => { if (stakeAmountPerPool[index] === 0n) { return null; } msgs.push(genStakeMsg(validatorAddress, stakeAmountPerPool[index])); }); } } const tx = { validUntil: (0, TonBaseStaker_1.defaultValidUntil)(validUntil), messages: msgs.filter((msg) => msg !== null) }; return { tx }; } /** * Builds an unstaking transaction for TON Pool contract. * * @param params - Parameters for building the transaction * @param params.delegatorAddress - The delegator address * @param params.validatorAddressPair - The validator address pair to unstake from * @param params.amount - The amount to unstake, specified in `TON`. When disableStatefulCalculation is true, must be a tuple [string, string] * @param params.disableStatefulCalculation - (Optional) Disables stateful calculation where validator and user stake is taken into account * @param params.validUntil - (Optional) The Unix timestamp when the transaction expires * * @returns Returns a promise that resolves to a TON nominator pool unstaking transaction. */ async buildUnstakeTx(params) { const { delegatorAddress, validatorAddressPair, amount, disableStatefulCalculation, validUntil } = params; // allow unstaking from a single pool const validatorAddresses = validatorAddressPair.filter((address) => address.length > 0); if (validatorAddresses.length == 0) { throw new Error('At least one validator address is required'); } validatorAddresses.forEach((validatorAddress) => { // ensure the address is for the right network this.checkIfAddressTestnetFlagMatches(validatorAddress); // ensure the validator address is bounceable. // NOTE: TEP-002 specifies that the address bounceable flag should match both the internal message and the address. // This has no effect as we force the bounce flag anyway. However it is a good practice to be consistent if (!ton_1.Address.parseFriendly(validatorAddress).isBounceable) { throw new Error('validator address is not bounceable! It is required for nominator pool contract operations to use bounceable addresses'); } }); const genUnstakeMsg = (validatorAddress, amount, withdrawFee, receiptPrice) => { // https://github.com/tonwhales/ton-nominators/blob/0553e1b6ddfc5c0b60505957505ce58d01bec3e7/compiled/nominators.fc#L20 const payload = (0, ton_1.beginCell)() .storeUint(3665837821, 32) // stake_withdraw method const .storeUint((0, TonBaseStaker_1.getRandomQueryId)(), 64) // Query ID .storeCoins((0, TonBaseStaker_1.getDefaultGas)()) // Gas .storeCoins(amount) // Amount .endCell(); return { address: validatorAddress, bounceable: true, amount: withdrawFee + receiptPrice, payload }; }; const poolParamsData = []; for (const validatorAddress of validatorAddresses) { const data = await this.getPoolParamsUnformatted({ validatorAddress }); poolParamsData.push(data); } const msgs = []; if (disableStatefulCalculation) { validatorAddresses.forEach((validatorAddress, index) => { const data = poolParamsData[index]; if (amount[index] === '') { return null; } msgs.push(genUnstakeMsg(validatorAddress, (0, ton_1.toNano)(amount[index]), data.withdrawFee, data.receiptPrice)); }); } else if (!Array.isArray(amount)) { const { minElectionStake, currentPoolBalances, userMaxUnstakeAmounts, userWithdraw } = await this.getPoolDataForDelegator(delegatorAddress, validatorAddresses); const unstakeAmountPerPool = TonPoolStaker.calculateUnstakePoolAmount((0, ton_1.toNano)(amount), minElectionStake, currentPoolBalances, userMaxUnstakeAmounts, [poolParamsData[0].minStake, poolParamsData[1].minStake], userWithdraw); if (unstakeAmountPerPool[0] + unstakeAmountPerPool[1] !== (0, ton_1.toNano)(amount)) { throw new Error('unstake amount does not match the requested amount'); } validatorAddresses.forEach((validatorAddress, index) => { const data = poolParamsData[index]; const amount = unstakeAmountPerPool[index]; // skip if no amount to unstake if (amount === 0n) { return null; } msgs.push(genUnstakeMsg(validatorAddress, amount, data.withdrawFee, data.receiptPrice)); }); } const tx = { validUntil: (0, TonBaseStaker_1.defaultValidUntil)(validUntil), messages: msgs.filter((msg) => msg !== null) }; return { tx }; } /** * Retrieves the staking information for a specified delegator. * * @param params - Parameters for the request * @param params.delegatorAddress - The delegator (wallet) address * @param params.validatorAddress - (Optional) The validator address to gather staking information from * * @returns Returns a promise that resolves to the staking information for the specified delegator. */ async getStake(params) { const { delegatorAddress, validatorAddress } = params; const client = this.getClient(); const response = await client.runMethod(ton_1.Address.parse(validatorAddress), 'get_member', [ { type: 'slice', cell: (0, ton_1.beginCell)().storeAddress(ton_1.Address.parse(delegatorAddress)).endCell() } ]); return { balance: (0, ton_1.fromNano)(response.stack.readBigNumber()), pendingDeposit: (0, ton_1.fromNano)(response.stack.readBigNumber()), pendingWithdraw: (0, ton_1.fromNano)(response.stack.readBigNumber()), withdraw: (0, ton_1.fromNano)(response.stack.readBigNumber()) }; } /** * Retrieves the staking information for a specified pool, including minStake and fees information. * * @param params - Parameters for the request * @param params.validatorAddress - The validator (vault) address * * @returns Returns a promise that resolves to the staking information for the specified pool. */ async getPoolParams(params) { const result = await this.getPoolParamsUnformatted(params); return { minStake: (0, ton_1.fromNano)(result.minStake), depositFee: (0, ton_1.fromNano)(result.depositFee), withdrawFee: (0, ton_1.fromNano)(result.withdrawFee), poolFee: (0, ton_1.fromNano)(result.poolFee), receiptPrice: (0, ton_1.fromNano)(result.receiptPrice) }; } /** * Retrieves the status of a transaction using the transaction hash. * * This method is intended to check for transactions made recently (within limit) and not for historical transactions. * * @param params - Parameters for the transaction status request * @param params.address - The account address to query * @param params.txHash - The transaction hash to query * @param params.limit - (Optional) The maximum number of transactions to fetch * * @returns A promise that resolves to an object containing the transaction status. */ async getTxStatus(params) { const transaction = await this.getTransactionByHash(params); if (transaction === undefined) { return { status: 'unknown', receipt: null }; } if (transaction.description.type === 'generic') { const description = transaction.description; if (description.computePhase.type === 'vm') { const compute = description.computePhase; if (compute.exitCode === 501) { return { status: 'failure', receipt: transaction, reason: 'withdraw_below_minimum_stake' }; } } } return this.matchTransactionStatus(transaction); } async getPoolParamsUnformatted(params) { const { validatorAddress } = params; const client = this.getClient(); const response = await client.runMethod(ton_1.Address.parse(validatorAddress), 'get_params', []); const data = { enabled: response.stack.readBoolean(), updatesEnables: response.stack.readBoolean(), minStake: response.stack.readBigNumber(), depositFee: response.stack.readBigNumber(), withdrawFee: response.stack.readBigNumber(), poolFee: response.stack.readBigNumber(), receiptPrice: response.stack.readBigNumber(), minStakeTotal: 0n }; data.minStakeTotal = data.minStake + data.depositFee + data.withdrawFee; return data; } /** @ignore */ async getPoolDataForDelegator(delegatorAddress, validatorAddresses) { const poolStatus = []; for (const validatorAddress of validatorAddresses) { const status = await this.getPoolStatus(validatorAddress); poolStatus.push(status); } const userStake = []; for (const validatorAddress of validatorAddresses) { const stake = await this.getStake({ delegatorAddress, validatorAddress }); userStake.push(stake); } const minElectionStake = await this.getElectionMinStake(); const currentPoolBalances = validatorAddresses.length === 2 ? [poolStatus[0].balance, poolStatus[1].balance] : [poolStatus[0].balance, 0n]; const userMaxUnstakeAmounts = validatorAddresses.length === 2 ? [ (0, ton_1.toNano)(userStake[0].balance) + (0, ton_1.toNano)(userStake[0].pendingDeposit) + (0, ton_1.toNano)(userStake[0].withdraw), (0, ton_1.toNano)(userStake[1].balance) + (0, ton_1.toNano)(userStake[1].pendingDeposit) + (0, ton_1.toNano)(userStake[1].withdraw) ] : [(0, ton_1.toNano)(userStake[0].balance) + (0, ton_1.toNano)(userStake[0].pendingDeposit) + (0, ton_1.toNano)(userStake[0].withdraw), 0n]; const userWithdraw = validatorAddresses.length === 2 ? [(0, ton_1.toNano)(userStake[0].withdraw), (0, ton_1.toNano)(userStake[1].withdraw)] : [(0, ton_1.toNano)(userStake[0].withdraw), 0n]; return { minElectionStake, currentPoolBalances, userMaxUnstakeAmounts, userWithdraw }; } async getElectionMinStake() { // elector contract address const elections = await this.getPastElections('Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF'); // simple sanity validation if (elections.length == 0) { throw new Error('No elections found'); } // iterate lastElection.frozen and find the lowest validator stake const lastElection = elections[0]; const values = Array.from(lastElection.frozen.values()); const minStake = values.reduce((min, p) => (p.stake < min ? p.stake : min), values[0].stake); return minStake; } async getPoolStatus(validatorAddress) { const client = this.getClient(); const provider = client.provider(ton_1.Address.parse(validatorAddress)); const res = await provider.get('get_pool_status', []); return { balance: res.stack.readBigNumber(), balanceSent: res.stack.readBigNumber(), balancePendingDeposits: res.stack.readBigNumber(), balancePendingWithdrawals: res.stack.readBigNumber(), balanceWithdraw: res.stack.readBigNumber() }; } async getPastElections(electorContractAddress) { const client = this.getClient(); const provider = client.provider(ton_1.Address.parse(electorContractAddress)); const res = await provider.get('past_elections', []); const FrozenDictValue = { serialize(_src, _builder) { throw Error('not implemented'); }, parse(src) { const address = new ton_1.Address(-1, src.loadBuffer(32)); const weight = src.loadUintBig(64); const stake = src.loadCoins(); return { address, weight, stake }; } }; // NOTE: In ideal case we would call `res.stack.readLispList()` however the library does not handle 'list' type well // and exits with an error. This is alternative way to get election data out of the 'list' type. const root = res.stack.readTuple(); const elections = []; while (root.remaining > 0) { const electionsEntry = root.pop(); const id = electionsEntry[0]; const unfreezeAt = electionsEntry[1]; const stakeHeld = electionsEntry[2]; const validatorSetHash = electionsEntry[3]; const frozenDict = electionsEntry[4]; const totalStake = electionsEntry[5]; const bonuses = electionsEntry[6]; const frozen = new Map(); const frozenData = frozenDict.beginParse().loadDictDirect(ton_1.Dictionary.Keys.Buffer(32), FrozenDictValue); for (const [key, value] of frozenData) { frozen.set(BigInt('0x' + key.toString('hex')).toString(10), { address: value['address'], weight: value['weight'], stake: value['stake'] }); } elections.push({ id, unfreezeAt, stakeHeld, validatorSetHash, totalStake, bonuses, frozen }); } // return elections sorted by id (bigint) in descending order return elections.sort((a, b) => (a.id > b.id ? -1 : 1)); } /** @ignore */ static selectPool(minStake, // minimum stake for participation (to be in the set) currentBalances // current stake balances of the pools ) { const [balancePool1, balancePool2] = currentBalances; const hasReachedMinStake = (balance) => balance >= minStake; // prioritize filling a pool that hasn't reached the minStake if (!hasReachedMinStake(balancePool1) && !hasReachedMinStake(balancePool2)) { // if neither pool has reached minStake, prioritize the one with the higher balance return balancePool1 >= balancePool2 ? 0 : 1; } else if (!hasReachedMinStake(balancePool1)) { return 0; // fill pool 1 to meet minStake } else if (!hasReachedMinStake(balancePool2)) { return 1; // fill pool 2 to meet minStake } // both pools have reached minStake, so allocate to the one with the lower balance return balancePool1 <= balancePool2 ? 0 : 1; } /** @ignore */ static selectStrategy(preferredStrategy, amount, totalValidators, lowestMinStake) { const strategy = preferredStrategy || 'balanced'; if (totalValidators === 0) { throw new Error('At least one validator address is required'); } if (totalValidators === 1) { return 'single'; } if (['split', 'balanced'].includes(strategy)) { const enoughStakeForBothPools = totalValidators > 1 && amount >= 2n * lowestMinStake; if (enoughStakeForBothPools) { return strategy; } return 'single'; } return strategy; } /** * Calculates optimal unstake amounts from two pools. * Tries strategies in order: keep both active → keep one active → deactivate both * * TODO: Add transaction simulation to catch false negatives thrown by SDK in case of bugs in calculation logic. * Consider adding anonymous telemetry/logging. * * TODO: Add `getValidUnstakeRanges()` method to help integrators validate amounts upfront by knowing the valid amounts to unstake. * */ static calculateUnstakePoolAmount(amount, // amount to unstake minElectionStake, // minimum stake for participation (to be in the set) [pool1Balance, pool2Balance], // current stake balances of the pools [pool1UserMaxUnstake, pool2UserMaxUnstake], // maximum user stake that can be unstaked from the pools [pool1MinStake, pool2MinStake], // min user stake per pool [pool1UserWithdraw, pool2UserWithdraw] // current user ready to withdraw from the pools ) { if (amount > pool1UserMaxUnstake + pool2UserMaxUnstake) { throw new Error('Requested amount exceeds available stakes'); } const buildPoolInfo = (index, poolBalance, userMaxUnstake, poolMinStake, userWithdraw) => { // userStaked = pending depositing + balance const userStaked = userMaxUnstake - userWithdraw; const maxUnstakeKeepPoolAboveMin = (userStaked > poolMinStake ? userStaked - poolMinStake : 0n) + userWithdraw; const maxUnstakeKeepPoolActive = (0, utils_1.minBigInt)((poolBalance > minElectionStake ? poolBalance - minElectionStake : 0n) + userWithdraw, maxUnstakeKeepPoolAboveMin); return { index, poolBalance, maxUnstakeAbsolute: userMaxUnstake, maxUnstakeKeepPoolActive, maxUnstakeKeepPoolAboveMin }; }; const poolInfos = [ buildPoolInfo(0, pool1Balance, pool1UserMaxUnstake, pool1MinStake, pool1UserWithdraw), buildPoolInfo(1, pool2Balance, pool2UserMaxUnstake, pool2MinStake, pool2UserWithdraw) ].sort((a, b) => Number(b.poolBalance - a.poolBalance)); // Sort by balance desc const [highBalPol, lowBalPol] = poolInfos; const unstakeAmounts = [0n, 0n]; const attemptPartialUnstakeFromBothPools = (primaryPool, primaryLimit, secondaryPool, secondaryLimit) => { if (primaryLimit + secondaryLimit < amount) return false; const fromPrimary = (0, utils_1.minBigInt)(amount, primaryLimit); const fromSecondary = amount - fromPrimary; unstakeAmounts[primaryPool.index] = fromPrimary; unstakeAmounts[secondaryPool.index] = fromSecondary; return true; }; const attemptCompleteUnstakeFromOnePool = (completeWithdrawalPool, partialWithdrawalPool, partialLimit) => { const completeAmount = completeWithdrawalPool.maxUnstakeAbsolute; if (completeAmount + partialLimit < amount || completeAmount > amount) { return false; } unstakeAmounts[completeWithdrawalPool.index] = completeAmount; unstakeAmounts[partialWithdrawalPool.index] = amount - completeAmount; return true; }; // Strategy 1: Complete withdrawal from both pools // Placed as first strategy to ensure full unstake requests are always accepted, providing a safety net in case the subsequent logic contains bugs. // It's not harmful for pool liveness because there is no way to optimize a full unstake. if (highBalPol.maxUnstakeAbsolute + lowBalPol.maxUnstakeAbsolute === amount) { unstakeAmounts[highBalPol.index] = highBalPol.maxUnstakeAbsolute; unstakeAmounts[lowBalPol.index] = lowBalPol.maxUnstakeAbsolute; return unstakeAmounts; } // Strategy 2: Keep both pools active if (attemptPartialUnstakeFromBothPools(highBalPol, highBalPol.maxUnstakeKeepPoolActive, lowBalPol, lowBalPol.maxUnstakeKeepPoolActive)) return unstakeAmounts; // Strategy 3: Keep higher balance pool active, deactivate lower balance pool if (attemptPartialUnstakeFromBothPools(highBalPol, highBalPol.maxUnstakeKeepPoolActive, lowBalPol, lowBalPol.maxUnstakeKeepPoolAboveMin)) return unstakeAmounts; if (attemptCompleteUnstakeFromOnePool(lowBalPol, highBalPol, highBalPol.maxUnstakeKeepPoolActive)) return unstakeAmounts; // Strategy 4: Keep lower balance pool active, deactivate higher balance pool if (attemptPartialUnstakeFromBothPools(lowBalPol, lowBalPol.maxUnstakeKeepPoolActive, highBalPol, highBalPol.maxUnstakeKeepPoolAboveMin)) return unstakeAmounts; if (attemptCompleteUnstakeFromOnePool(highBalPol, lowBalPol, lowBalPol.maxUnstakeKeepPoolActive)) return unstakeAmounts; // Strategy 5: Deactivate both pools but maintain minimum stakes. Contract-native logic for valid remaining staked amount: https://github.com/ChorusOne/ton-pool-contracts/blob/fa98fb53556bad6f03db2adf84476a16502de6bf/nominators.fc#L1014 if (attemptPartialUnstakeFromBothPools(highBalPol, highBalPol.maxUnstakeKeepPoolAboveMin, lowBalPol, lowBalPol.maxUnstakeKeepPoolAboveMin)) return unstakeAmounts; if (attemptCompleteUnstakeFromOnePool(lowBalPol, highBalPol, highBalPol.maxUnstakeKeepPoolAboveMin)) return unstakeAmounts; if (attemptCompleteUnstakeFromOnePool(highBalPol, lowBalPol, lowBalPol.maxUnstakeKeepPoolAboveMin)) return unstakeAmounts; throw new Error('No valid combination to unstake requested amount'); } /** @ignore */ static calculateStakePoolAmount(amount, // amount to stake minStake, // minimum stake for participation (to be in the set) currentPoolBalances, // current stake balances of the pools minPoolStakes // min staked amount per pool ) { const [poolOneBalance, poolTwoBalance] = currentPoolBalances; const [minPoolOne, minPoolTwo] = minPoolStakes; // Every stake has to be greater than the minStake: https://github.com/ChorusOne/ton-pool-contracts/blob/fa98fb53556bad6f03db2adf84476a16502de6bf/nominators.fc#L958 if (amount < minPoolOne || amount < minPoolTwo) { throw new Error('amount is less than the minimum required to stake'); } const calculate = () => { const result = [0n, 0n]; // case: both pools are at or above minStake const poolOneAboveMin = poolOneBalance >= minStake; const poolTwoAboveMin = poolTwoBalance >= minStake; // here we know that both pools will get elected therefore // we should balance the user stake to equalize the pool balances if (poolOneAboveMin && poolTwoAboveMin) { const highestStakeI = poolOneBalance > poolTwoBalance ? 0 : 1; const lowerStakeI = highestStakeI === 1 ? 0 : 1; const stakedDelta = currentPoolBalances[highestStakeI] - currentPoolBalances[lowerStakeI]; const remainder = amount - stakedDelta; // if the amount won't balance two stakes, we add the amount to the // lowest stake to fill the gap as much as possible if (remainder <= 0n) { result[lowerStakeI] = amount; result[highestStakeI] = 0n; return result; } // if the remainder is less than min, then splitting it 50/50 will // not work. Instead stake all to one pool if (remainder < minPoolOne || remainder < minPoolTwo) { result[highestStakeI] = 0n; result[lowerStakeI] = stakedDelta + remainder; return result; } // now ideal case is to split the remainder 50/50, but if that is not // possible due to minPool stake constraints, we need to adjust const halfRemainder = remainder / 2n; if (halfRemainder <= minPoolOne || halfRemainder <= minPoolTwo) { if (stakedDelta < minPoolOne || stakedDelta < minPoolTwo) { // balancing out without going below minPool is impossible // split the stake amount 50/50 instead result[highestStakeI] = amount / 2n; result[lowerStakeI] = amount - result[highestStakeI]; } else { // carry over the remainder to the higher stake pool, // because reminder can't be split 50/50 without violating minPool // constraint result[highestStakeI] = remainder; result[lowerStakeI] = stakedDelta; } return result; } // here most likely we have enough tokens to balance and split the // remainder 50/50 result[highestStakeI] = remainder / 2n; result[lowerStakeI] = stakedDelta + remainder - remainder / 2n; return result; } const poolOneBelowMin = poolOneBalance < minStake && poolTwoBalance >= minStake; const poolTwoBelowMin = poolTwoBalance < minStake && poolOneBalance >= minStake; // case: one pool is below minStake and one is above if (poolOneBelowMin || poolTwoBelowMin) { const needed = [minStake - poolOneBalance, minStake - poolTwoBalance]; const highestStakeI = poolOneBalance > poolTwoBalance ? 0 : 1; const lowerStakeI = highestStakeI === 1 ? 0 : 1; // the pool will become active if (needed[lowerStakeI] - amount <= 0) { const remaining = amount - needed[highestStakeI]; result[highestStakeI] = needed[highestStakeI] + remaining / 2n; result[lowerStakeI] = remaining - remaining / 2n; return result; } // no chance of filling the pool to become active const remaining = amount; result[lowerStakeI] = remaining / 2n; result[highestStakeI] = remaining - remaining / 2n; return result; } // case: both pools are below minStake if (!poolOneAboveMin && !poolTwoAboveMin) { const needed = [minStake - poolOneBalance, minStake - poolTwoBalance]; const highestStakeI = poolOneBalance > poolTwoBalance ? 0 : 1; const lowerStakeI = highestStakeI === 1 ? 0 : 1; // there is a chance to make both pools active if (amount >= needed[0] + needed[1]) { const remaining = amount - (needed[0] + needed[1]); result[0] = needed[0] + remaining / 2n; result[1] = needed[1] + remaining - remaining / 2n; return result; } // at least one pool will be active if (needed[0] - amount <= 0 || needed[1] - amount <= 0) { const remaining = amount - needed[highestStakeI]; result[highestStakeI] = needed[highestStakeI] + remaining / 2n; result[lowerStakeI] = remaining - remaining / 2n; return result; } // no chance of filling both pools to become active result[highestStakeI] = amount; return result; } // fallback: split 50/50 result[0] = amount / 2n; result[1] = amount - result[0]; return result; }; const fallback = (result) => { // if both amounts are lower than minPool (0n may be explicit action), something must hve gone wrong // attempt to split the amount 50/50 and hope for the best if ((result[0] !== 0n && result[0] < minPoolOne) || (result[1] !== 0n && result[1] < minPoolTwo)) { result[0] = amount / 2n; result[1] = amount - result[0]; } return result; }; const stakeAmountPerPool = fallback(calculate()); // sanity check sum if (stakeAmountPerPool.reduce((acc, val) => acc + val, 0n) !== amount) { throw new Error('stake amount does not match the requested amount, this should not have happened'); } // sanity check negative values if (stakeAmountPerPool.some((stake) => stake < 0n)) { throw new Error('stake amount per pool cannot be negative, this should not have happened'); } return stakeAmountPerPool; } } exports.TonPoolStaker = TonPoolStaker;