Skip to content

Commit

Permalink
Merge pull request #145 from TxnLab/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
drichar authored May 16, 2024
2 parents 11f0703 + 8b89478 commit 4dc845c
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 32 deletions.
2 changes: 1 addition & 1 deletion contracts/bootstrap/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bootstrap",
"version": "0.8.4",
"version": "0.8.5",
"description": "",
"main": "index.ts",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "reti-contracts",
"version": "0.8.4",
"version": "0.8.5",
"license": "MIT",
"scripts": {
"generate-client": "algokit generate client contracts/artifacts/ --language typescript --output contracts/clients/{contract_name}Client.ts && ./update_contract_artifacts.sh``",
Expand Down
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "reti-ui",
"version": "0.8.4",
"version": "0.8.5",
"author": {
"name": "Doug Richar",
"email": "[email protected]"
Expand Down
43 changes: 43 additions & 0 deletions ui/src/api/algod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount'
import {
AccountBalance,
AccountInformation,
AlgodHttpError,
Asset,
AssetCreatorHolding,
AssetHolding,
Expand Down Expand Up @@ -67,6 +68,48 @@ export async function fetchAssetHoldings(address: string | null): Promise<AssetH
return assets
}

export async function fetchAccountAssetInformation(
address: string | null,
assetId: number,
): Promise<AssetHolding> {
if (!address) {
throw new Error('No address provided')
}
if (!assetId) {
throw new Error('No assetId provided')
}
try {
const assetHolding = await algodClient.accountAssetInformation(address, assetId).do()
return assetHolding as AssetHolding
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error.status && error.body?.message) {
throw new AlgodHttpError({
status: error.status,
body: error.body,
headers: error.headers,
ok: error.ok,
text: error.text,
})
} else {
throw error
}
}
}

export async function isOptedInToAsset(address: string | null, assetId: number): Promise<boolean> {
try {
await fetchAccountAssetInformation(address, assetId)
return true
} catch (error: unknown) {
if (error instanceof AlgodHttpError && error.status === 404) {
return false
} else {
throw error
}
}
}

export async function fetchAssetCreatorHoldings(
address: string | null,
): Promise<AssetCreatorHolding[]> {
Expand Down
95 changes: 77 additions & 18 deletions ui/src/api/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as algokit from '@algorandfoundation/algokit-utils'
import { TransactionSignerAccount } from '@algorandfoundation/algokit-utils/types/account'
import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount'
import algosdk from 'algosdk'
import { isOptedInToAsset } from '@/api/algod'
import {
getSimulateStakingPoolClient,
getSimulateValidatorClient,
Expand Down Expand Up @@ -521,6 +522,7 @@ export async function addStake(
validatorId: number,
stakeAmount: number, // microalgos
valueToVerify: number,
rewardTokenId: number,
signer: algosdk.TransactionSigner,
activeAddress: string,
authAddr?: string,
Expand All @@ -537,9 +539,19 @@ export async function addStake(
suggestedParams,
})

const rewardTokenOptInTxn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({
from: activeAddress,
to: activeAddress,
amount: 0,
assetIndex: rewardTokenId,
suggestedParams,
})

const needsOptInTxn = rewardTokenId > 0 && !(await isOptedInToAsset(activeAddress, rewardTokenId))

const simulateValidatorClient = await getSimulateValidatorClient(activeAddress, authAddr)

const simulateResults = await simulateValidatorClient
const simulateComposer = simulateValidatorClient
.compose()
.gas({})
.addStake(
Expand All @@ -553,16 +565,25 @@ export async function addStake(
},
{ sendParams: { fee: AlgoAmount.MicroAlgos(240_000) } },
)
.simulate({ allowEmptySignatures: true, allowUnnamedResources: true })

if (needsOptInTxn) {
simulateComposer.addTransaction(rewardTokenOptInTxn)
}

const simulateResults = await simulateComposer.simulate({
allowEmptySignatures: true,
allowUnnamedResources: true,
})

stakeTransferPayment.group = undefined
rewardTokenOptInTxn.group = undefined

// @todo: switch to Joe's new method(s)
const feesAmount = AlgoAmount.MicroAlgos(
const feeAmount = AlgoAmount.MicroAlgos(
2000 + 1000 * ((simulateResults.simulateResponse.txnGroups[0].appBudgetAdded as number) / 700),
)

const results = await validatorClient
const composer = validatorClient
.compose()
.gas({})
.addStake(
Expand All @@ -574,11 +595,16 @@ export async function addStake(
validatorId,
valueToVerify,
},
{ sendParams: { fee: feesAmount } },
{ sendParams: { fee: feeAmount } },
)
.execute({ populateAppCallResources: true })

const [valId, poolId, poolAppId] = results.returns![1]
if (needsOptInTxn) {
composer.addTransaction(rewardTokenOptInTxn)
}

const result = await composer.execute({ populateAppCallResources: true })

const [valId, poolId, poolAppId] = result.returns![1]

return {
poolId: Number(poolId),
Expand Down Expand Up @@ -629,7 +655,12 @@ export async function fetchStakedPoolsForAccount(staker: string): Promise<Valida

const stakedPools = result.returns![0]

return stakedPools.map(([validatorId, poolId, poolAppId]) => ({
// Filter out potential duplicates (temporary UI fix for duplicate staked pools bug)
const uniqueStakedPools = Array.from(
new Set(stakedPools.map((sp) => JSON.stringify(sp.map((v) => Number(v))))),
).map((sp) => JSON.parse(sp) as (typeof stakedPools)[0])

return uniqueStakedPools.map(([validatorId, poolId, poolAppId]) => ({
validatorId: Number(validatorId),
poolId: Number(poolId),
poolAppId: Number(poolAppId),
Expand Down Expand Up @@ -795,17 +826,30 @@ export async function fetchProtocolConstraints(
export async function removeStake(
poolAppId: number | bigint,
amountToUnstake: number,
rewardTokenId: number,
signer: algosdk.TransactionSigner,
activeAddress: string,
authAddr?: string,
) {
const suggestedParams = await ParamsCache.getSuggestedParams()

const rewardTokenOptInTxn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({
from: activeAddress,
to: activeAddress,
amount: 0,
assetIndex: rewardTokenId,
suggestedParams,
})

const needsOptInTxn = rewardTokenId > 0 && !(await isOptedInToAsset(activeAddress, rewardTokenId))

const stakingPoolSimulateClient = await getSimulateStakingPoolClient(
poolAppId,
activeAddress,
authAddr,
)

const simulateResult = await stakingPoolSimulateClient
const simulateComposer = stakingPoolSimulateClient
.compose()
.gas({}, { note: '1', sendParams: { fee: AlgoAmount.MicroAlgos(0) } })
.gas({}, { note: '2', sendParams: { fee: AlgoAmount.MicroAlgos(0) } })
Expand All @@ -816,19 +860,29 @@ export async function removeStake(
},
{ sendParams: { fee: AlgoAmount.MicroAlgos(240_000) } },
)
.simulate({ allowEmptySignatures: true, allowUnnamedResources: true })

if (needsOptInTxn) {
simulateComposer.addTransaction(rewardTokenOptInTxn)
}

const simulateResult = await simulateComposer.simulate({
allowEmptySignatures: true,
allowUnnamedResources: true,
})

// @todo: switch to Joe's new method(s)
const feesAmount = AlgoAmount.MicroAlgos(
const feeAmount = AlgoAmount.MicroAlgos(
1000 *
Math.floor(
((simulateResult.simulateResponse.txnGroups[0].appBudgetAdded as number) + 699) / 700,
),
)

rewardTokenOptInTxn.group = undefined

const stakingPoolClient = await getStakingPoolClient(poolAppId, signer, activeAddress)

await stakingPoolClient
const composer = stakingPoolClient
.compose()
.gas({}, { note: '1', sendParams: { fee: AlgoAmount.MicroAlgos(0) } })
.gas({}, { note: '2', sendParams: { fee: AlgoAmount.MicroAlgos(0) } })
Expand All @@ -837,9 +891,14 @@ export async function removeStake(
staker: activeAddress,
amountToUnstake,
},
{ sendParams: { fee: feesAmount } },
{ sendParams: { fee: feeAmount } },
)
.execute({ populateAppCallResources: true })

if (needsOptInTxn) {
composer.addTransaction(rewardTokenOptInTxn)
}

await composer.execute({ populateAppCallResources: true })
}

export async function epochBalanceUpdate(
Expand All @@ -863,7 +922,7 @@ export async function epochBalanceUpdate(
.simulate({ allowEmptySignatures: true, allowUnnamedResources: true })

// @todo: switch to Joe's new method(s)
const feesAmount = AlgoAmount.MicroAlgos(
const feeAmount = AlgoAmount.MicroAlgos(
3000 + 1000 * ((simulateResult.simulateResponse.txnGroups[0].appBudgetAdded as number) / 700),
)

Expand All @@ -873,7 +932,7 @@ export async function epochBalanceUpdate(
.compose()
.gas({}, { note: '1', sendParams: { fee: AlgoAmount.MicroAlgos(0) } })
.gas({}, { note: '2', sendParams: { fee: AlgoAmount.MicroAlgos(0) } })
.epochBalanceUpdate({}, { sendParams: { fee: feesAmount } })
.epochBalanceUpdate({}, { sendParams: { fee: feeAmount } })
.execute({ populateAppCallResources: true })
} catch (error) {
console.error(error)
Expand Down Expand Up @@ -992,7 +1051,7 @@ export async function claimTokens(
)

// @todo: switch to Joe's new method(s)
const feesAmount = AlgoAmount.MicroAlgos(
const feeAmount = AlgoAmount.MicroAlgos(
1000 *
Math.floor(
((simulateResult.simulateResponse.txnGroups[0].appBudgetAdded as number) + 699) / 700,
Expand All @@ -1005,7 +1064,7 @@ export async function claimTokens(
const client = await getStakingPoolClient(pool.poolAppId, signer, activeAddress)
await client.gas({}, { note: '1', sendParams: { atc: atc2, fee: AlgoAmount.MicroAlgos(0) } })
await client.gas({}, { note: '2', sendParams: { atc: atc2, fee: AlgoAmount.MicroAlgos(0) } })
await client.claimTokens({}, { sendParams: { atc: atc2, fee: feesAmount } })
await client.claimTokens({}, { sendParams: { atc: atc2, fee: feeAmount } })
}

await algokit.sendAtomicTransactionComposer(
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/AddStakeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export function AddStakeModal({
validator!.id,
totalAmount,
valueToVerify,
validator!.config.rewardTokenId,
transactionSigner,
activeAddress,
authAddress,
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/UnstakeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export function UnstakeModal({ validator, setValidator, stakesByValidator }: Uns
await removeStake(
pool.poolKey.poolAppId,
amountToUnstake,
validator!.config.rewardTokenId,
transactionSigner,
activeAddress,
authAddress,
Expand Down
26 changes: 26 additions & 0 deletions ui/src/interfaces/algod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ export interface Asset {
params: AssetParams
}

export class AlgodHttpError extends Error {
public status: number
public body: {
message: string
}
public headers: Record<string, string>
public ok: boolean
public text: string

constructor(response: {
status: number
body: { message: string }
headers: Record<string, string>
ok: boolean
text: string
}) {
super(response.body.message)
this.name = 'AlgodHttpError'
this.status = response.status
this.body = response.body
this.headers = response.headers
this.ok = response.ok
this.text = response.text
}
}

export interface NodeStatusResponse {
/**
* CatchupTime in nanoseconds
Expand Down
16 changes: 8 additions & 8 deletions ui/src/utils/contracts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,10 @@ describe('calculateRewardEligibility', () => {
expect(calculateRewardEligibility(10, 1000, undefined)).toBeNull()
})

it('should calculate correct percentage when entry round and payout are in the past', () => {
it('should calculate correct percentage when entry round is halfway through an epoch', () => {
const epochRoundLength = 100
const lastPoolPayoutRound = 900
const entryRound = 850
const entryRound = 950
expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(50)
})

Expand All @@ -290,23 +290,23 @@ describe('calculateRewardEligibility', () => {
})

it('should round down to the nearest integer', () => {
const epochRoundLength = 100
const lastPoolPayoutRound = 300
const entryRound = 251 // Exact eligibility is 49%
expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(49)
const epochRoundLength = 200
const lastPoolPayoutRound = 600
const entryRound = 651 // Exact eligibility is 74.5%
expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(74)
})

it('should never return more than 100%', () => {
const epochRoundLength = 100
const lastPoolPayoutRound = 300
const entryRound = 200
const entryRound = 200 // Calculated eligibility is 200%
expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(100)
})

it('should never return less than 0%', () => {
const epochRoundLength = 100
const lastPoolPayoutRound = 100
const entryRound = 200 // Future round beyond the current epoch
const entryRound = 250 // Future round beyond the current epoch
expect(calculateRewardEligibility(epochRoundLength, lastPoolPayoutRound, entryRound)).toBe(0)
})
})
Loading

0 comments on commit 4dc845c

Please sign in to comment.