@chorus-one/ton
Version:
All-in-one tooling for building staking dApps on TON
206 lines (179 loc) • 8.93 kB
text/typescript
import { Address, toNano, fromNano, TupleReader, TupleItem } from '@ton/ton'
import { defaultValidUntil, TonBaseStaker } from './TonBaseStaker'
import { NominatorInfo, UnsignedTx } from './types'
export class TonNominatorPoolStaker extends TonBaseStaker {
/**
* Builds a staking (delegation) transaction for Nominator Pool contract.
* For more information see: https://github.com/ton-blockchain/nominator-pool
*
* @param params - Parameters for building the transaction
* @param params.delegatorAddress - The delegator address to stake from
* @param params.validatorAddress - The validator address to stake to
* @param params.amount - The amount to stake, specified in `TON`
* @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: {
delegatorAddress: string
validatorAddress: string
amount: string
validUntil?: number
}): Promise<{ tx: UnsignedTx }> {
const { delegatorAddress, validatorAddress, amount, validUntil } = params
// 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 (!Address.parseFriendly(validatorAddress).isBounceable) {
throw new Error(
'validator address is not bounceable! It is required for nominator pool contract operations to use bounceable addresses'
)
}
// this is also a somewhat okay way to check the contract is indeed a staking pool contract
const data = await this.getNominatorContractPoolData(validatorAddress)
if (data.nominators_count >= data.max_nominators_count) {
throw new Error('validator has reached the maximum number of nominators')
}
// ensure we stake at least the minimum required amount
const nominators = (await this.getPoolContractNominators({ validatorAddress })).nominators
const ourNominator = nominators.find((n) => Address.parseRaw(n.address).equals(Address.parse(delegatorAddress)))
const amountStaked = ourNominator ? toNano(ourNominator.amount) : BigInt(0)
const amountToStake = toNano(amount)
if (amountToStake + amountStaked < data.min_nominator_stake) {
throw new Error(
`amount to stake (${fromNano(amountToStake)}) is less than the minimum stake required (${fromNano(data.min_nominator_stake)})`
)
}
const tx = {
validUntil: defaultValidUntil(validUntil),
messages: [
{
address: validatorAddress,
// to stake tokens we need to send a large amount of tokens
// it is critical that the transaction is bounceable
// otherwise in the case of contract failure we may loose tokens!
bounceable: true,
amount: toNano(amount),
payload: 'd' // 'd' for deposit / delegation
}
]
}
return { tx }
}
/**
* Builds an unstaking (withdraw nominator) transaction for Nominator Pool contract.
* For more information see: https://github.com/ton-blockchain/nominator-pool
*
* @param params - Parameters for building the transaction
* @param params.delegatorAddress - The delegator address
* @param params.validatorAddress - The validator address to unstake from
* @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: {
delegatorAddress: string
validatorAddress: string
validUntil?: number
}): Promise<{ tx: UnsignedTx }> {
const { delegatorAddress, validatorAddress, validUntil } = params
// "In order for the nominator to make a withdrawal, he needs to send message to nominator-pool smart contract with text comment "w"
// and some Toncoins for network fee (1 TON is enough). Unspent TONs attached to message will be returned except in very rare cases."
//
// source: https://github.com/ton-blockchain/nominator-pool?tab=readme-ov-file#nominators-withdrawal
const amount = '1' // 1 TON
// 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 (!Address.parseFriendly(validatorAddress).isBounceable) {
throw new Error(
'validator address is not bounceable! It is required for nominator pool contract operations to use bounceable addresses'
)
}
// this is also a somewhat okay way to check the contract is indeed a staking pool contract
const data = await this.getNominatorContractPoolData(validatorAddress)
if (data.nominators_count === 0) {
throw new Error('there is no nominators currently staking to the nominator pool contract')
}
// ensure that the delegator has staked to the validator
const nominators = (await this.getPoolContractNominators({ validatorAddress })).nominators
const ourNominator = nominators.find((n) => Address.parseRaw(n.address).equals(Address.parse(delegatorAddress)))
if (!ourNominator) {
throw new Error('delegator is not staking to the nominator pool contract')
}
const tx = {
validUntil: defaultValidUntil(validUntil),
messages: [
{
address: validatorAddress,
// to unstake tokens we need to send a some tokens that should
// be returned to us in case of error
bounceable: true,
amount: toNano(amount),
payload: 'w' // 'w' for withdraw
}
]
}
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 - The validator address to gather rewards data from
* @param params.contractType - The validator contract type (single-nominator-pool or nominator-pool)
*
* @returns Returns a promise that resolves to the staking information for the specified delegator.
*/
async getStake (params: { delegatorAddress: string; validatorAddress: string }): Promise<{ balance: string }> {
const { delegatorAddress, validatorAddress } = params
const { nominators } = await this.getPoolContractNominators({ validatorAddress })
if (nominators.length === 0) {
return { balance: '0' }
}
const nominator = nominators.find((n) => Address.parse(n.address).equals(Address.parse(delegatorAddress)))
if (nominator === undefined) {
return { balance: '0' }
}
return { balance: nominator.amount }
}
/**
* Retrieves the active nominators for a Nominator Pool contract.
* For more information see: https://github.com/ton-blockchain/nominator-pool
*
* @param params - Parameters for the request
* @param params.validatorAddress - The validator address to gather rewards data from
*
* @returns Returns a promise that resolves to the nominator data for the validator address.
*/
async getPoolContractNominators (params: { validatorAddress: string }): Promise<{ nominators: NominatorInfo[] }> {
const client = this.getClient()
const { validatorAddress } = params
// ensure the address is for the right network
this.checkIfAddressTestnetFlagMatches(validatorAddress)
const response = await client.runMethod(Address.parse(validatorAddress), 'list_nominators', [])
// @ts-expect-error the library does not handle 'list' type well. This is a workaround to get the data out of the 'list' type
const reader = new TupleReader(response.stack.pop().items as TupleItem[])
// extract nominators from contract response
const nominators: NominatorInfo[] = []
if (reader.remaining > 0) {
do {
const x = reader.readTuple()
nominators.push({
// The nominator pool contract allows only the basechain addresses (`0:`)
// https://github.com/ton-blockchain/nominator-pool/blob/main/func/pool.fc#L618
address: `0:${BigInt(x.readBigNumber()).toString(16)}`,
amount: fromNano(x.readBigNumber()),
pending_deposit_amount: fromNano(x.readBigNumber()),
withdraw_requested: fromNano(x.readBigNumber())
})
} while (reader.remaining)
}
return { nominators }
}
}