From 6f23ceedff49f1b8bda46af41b32ace583f2ecda Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Fri, 25 Apr 2025 19:21:49 +0300 Subject: [PATCH 01/60] first draft --- packages/node/bin/autostaker-manual-test.sh | 52 +++++++++++ packages/node/src/config/validateConfig.ts | 1 + packages/node/src/pluginRegistry.ts | 3 + .../plugins/autostaker/AutostakerPlugin.ts | 87 +++++++++++++++++++ .../src/plugins/autostaker/config.schema.json | 29 +++++++ 5 files changed, 172 insertions(+) create mode 100755 packages/node/bin/autostaker-manual-test.sh create mode 100644 packages/node/src/plugins/autostaker/AutostakerPlugin.ts create mode 100644 packages/node/src/plugins/autostaker/config.schema.json diff --git a/packages/node/bin/autostaker-manual-test.sh b/packages/node/bin/autostaker-manual-test.sh new file mode 100755 index 0000000000..d2e9e25b8a --- /dev/null +++ b/packages/node/bin/autostaker-manual-test.sh @@ -0,0 +1,52 @@ +# Do not merge this manual test to main + +NODE_PRIVATE_KEY="1111111111111111111111111111111111111111111111111111111111111111" +OWNER_PRIVATE_KEY="2222222222222222222222222222222222222222222222222222222222222222" +SPONSORER_PRIVATE_KEY="3333333333333333333333333333333333333333333333333333333333333333" +EARNINGS_PER_SECOND=12 +STAKED_AMOUNT=500000 +SPONSOR_AMOUNT=1234567 + +NODE_ADDRESS=$(ethereum-address $NODE_PRIVATE_KEY | jq -r '.address') +OWNER_ADDRESS=$(ethereum-address $OWNER_PRIVATE_KEY | jq -r '.address') +SPONSORER_ADDRESS=$(ethereum-address $SPONSORER_PRIVATE_KEY | jq -r '.address') + +cd ../../cli-tools +echo 'Mint tokens' +npx tsx bin/streamr.ts internal token-mint $NODE_ADDRESS 10000000 10000000 +npx tsx bin/streamr.ts internal token-mint $OWNER_ADDRESS 10000000 10000000 +echo 'Create operator' +OPERATOR_CONTRACT_ADDRESS=$(npx tsx bin/streamr.ts internal operator-create -c 10 --node-addresses $NODE_ADDRESS --env dev2 --private-key $OWNER_PRIVATE_KEY | jq -r '.address') +npx tsx bin/streamr.ts internal token-mint $SPONSORER_ADDRESS 10000000 10000000 +npx tsx bin/streamr.ts stream create /foo --env dev2 --private-key $SPONSORER_PRIVATE_KEY +SPONSORSHIP_CONTRACT_ADDRESS=$(npx tsx bin/streamr.ts internal sponsorship-create /foo -e $EARNINGS_PER_SECOND --env dev2 --private-key $SPONSORER_PRIVATE_KEY | jq -r '.address') +npx tsx bin/streamr.ts internal sponsorship-sponsor $SPONSORSHIP_CONTRACT_ADDRESS $SPONSOR_AMOUNT --env dev2 --private-key $SPONSORER_PRIVATE_KEY +npx tsx bin/streamr.ts internal operator-delegate $OPERATOR_CONTRACT_ADDRESS $STAKED_AMOUNT --env dev2 --private-key $OWNER_PRIVATE_KEY + +jq -n \ + --arg nodePrivateKey "$NODE_PRIVATE_KEY" \ + --arg operatorOwnerPrivateKey "$OWNER_PRIVATE_KEY" \ + --arg operatorContractAddress "$OPERATOR_CONTRACT_ADDRESS" \ + '{ + "$schema": "https://schema.streamr.network/config-v3.schema.json", + client: { + auth: { + privateKey: $nodePrivateKey + }, + environment: "dev2" + }, + plugins: { + autostaker: { + operatorOwnerPrivateKey: $operatorOwnerPrivateKey, + operatorContractAddress: $operatorContractAddress + } + } + }' > ../node/configs/autostaker.json + +jq -n \ + --arg operatorContract "$OPERATOR_CONTRACT_ADDRESS" \ + --arg sponsorshipContract "$SPONSORSHIP_CONTRACT_ADDRESS" \ + '$ARGS.named' + +cd ../node +npx tsx bin/streamr-node.ts configs/autostaker.json diff --git a/packages/node/src/config/validateConfig.ts b/packages/node/src/config/validateConfig.ts index 8560c650e8..30fce45d09 100644 --- a/packages/node/src/config/validateConfig.ts +++ b/packages/node/src/config/validateConfig.ts @@ -9,6 +9,7 @@ export const validateConfig = (data: unknown, schema: Schema, contextName?: stri }) addFormats(ajv) ajv.addFormat('ethereum-address', /^0x[a-zA-Z0-9]{40}$/) + ajv.addFormat('ethereum-private-key', /^(0x)?[a-zA-Z0-9]{64}$/) ajv.addSchema(DEFINITIONS_SCHEMA) if (!ajv.validate(schema, data)) { const prefix = (contextName !== undefined) ? (contextName + ': ') : '' diff --git a/packages/node/src/pluginRegistry.ts b/packages/node/src/pluginRegistry.ts index 1029d27d4c..2c795c5c35 100644 --- a/packages/node/src/pluginRegistry.ts +++ b/packages/node/src/pluginRegistry.ts @@ -1,5 +1,6 @@ import { Plugin } from './Plugin' import { StrictConfig } from './config/config' +import { AutostakerPlugin } from './plugins/autostaker/AutostakerPlugin' import { ConsoleMetricsPlugin } from './plugins/consoleMetrics/ConsoleMetricsPlugin' import { HttpPlugin } from './plugins/http/HttpPlugin' import { InfoPlugin } from './plugins/info/InfoPlugin' @@ -27,6 +28,8 @@ export const createPlugin = (name: string, brokerConfig: StrictConfig): Plugin { + + private abortController: AbortController = new AbortController() + + async start(streamrClient: StreamrClient): Promise { + logger.info('Start autostaker plugin') + scheduleAtInterval(async () => { + try { + await this.runActions(streamrClient) + } catch (err) { + logger.warn('Error while running autostaker actions', { err }) + } + }, this.pluginConfig.runIntervalInMs, false, this.abortController.signal) + } + + private async runActions(streamrClient: StreamrClient): Promise { + logger.info('Run autostaker actions') + const provider = (await streamrClient.getSigner()).provider + const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) + .connect(new Wallet(this.pluginConfig.operatorOwnerPrivateKey, provider)) + const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() + const availableBalance = (await operatorContract.valueWithoutEarnings()) - stakedAmount + logger.info(`Available balance: ${formatEther(availableBalance)} (staked=${formatEther(stakedAmount)})`) + // TODO is there a better way to get the client? Maybe we should add StreamrClient#getTheGraphClient() + // TODO what are good where consitions for the sponsorships query + // @ts-expect-error private + const queryResult = streamrClient.theGraphClient.queryEntities((lastId: string, pageSize: number) => { + return { + query: ` + { + sponsorships( + where: { + projectedInsolvency_gt: ${Math.floor(Date.now() / 1000)}, + spotAPY_gt: 0 + id_gt: "${lastId}" + }, + first: ${pageSize} + ) { + id + } + } + ` + } + }) + const sponsorships: { id: string }[] = await collect(queryResult) + logger.info(`Available sponsorships: ${sponsorships.map((s) => s.id).join(',')}`) + if ((sponsorships.length) > 0 && (availableBalance >= STAKE_AMOUNT)) { + const targetSponsorship = sample(sponsorships)! + logger.info(`Stake ${formatEther(STAKE_AMOUNT)} to ${targetSponsorship.id}`) + await _operatorContractUtils.stake( + operatorContract, + targetSponsorship.id, + STAKE_AMOUNT + ) + } + } + + async stop(): Promise { + logger.info('Stop autostaker plugin') + this.abortController.abort() + } + + // eslint-disable-next-line class-methods-use-this + override getConfigSchema(): Schema { + return PLUGIN_CONFIG_SCHEMA + } +} diff --git a/packages/node/src/plugins/autostaker/config.schema.json b/packages/node/src/plugins/autostaker/config.schema.json new file mode 100644 index 0000000000..f418d2e570 --- /dev/null +++ b/packages/node/src/plugins/autostaker/config.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Autostaker plugin configuration", + "additionalProperties": false, + "required": [ + "operatorContractAddress", + "operatorOwnerPrivateKey" + ], + "properties": { + "operatorContractAddress": { + "type": "string", + "description": "Operator contract Ethereum address", + "format": "ethereum-address" + }, + "operatorOwnerPrivateKey": { + "type": "string", + "description": "Operator owner's private key", + "format": "ethereum-private-key" + }, + "runIntervalInMs": { + "type": "integer", + "description": "The interval (in milliseconds) at which autostaking possibilities are analyzed and executed", + "minimum": 0, + "default": 30000 + } + } +} From d4968e593638d59a53e6b1927b8f7fc237af4239 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 19 May 2025 11:08:01 +0300 Subject: [PATCH 02/60] payoutProportional strategy (mostly copy-paste from autostaker repo) --- .../plugins/autostaker/AutostakerPlugin.ts | 110 ++++++++-- .../autostaker/payoutProportionalStrategy.ts | 113 ++++++++++ packages/node/src/plugins/autostaker/types.ts | 46 ++++ .../payoutProportionalStrategy.test.ts | 206 ++++++++++++++++++ packages/utils/src/exports.ts | 1 + packages/utils/src/sum.ts | 3 + 6 files changed, 459 insertions(+), 20 deletions(-) create mode 100644 packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts create mode 100644 packages/node/src/plugins/autostaker/types.ts create mode 100644 packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts create mode 100644 packages/utils/src/sum.ts diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 6df364d502..af7c4a08ff 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -1,10 +1,11 @@ import { _operatorContractUtils, SignerWithProvider, StreamrClient } from '@streamr/sdk' import { collect, Logger, scheduleAtInterval, WeiAmount } from '@streamr/utils' import { Schema } from 'ajv' -import { formatEther, parseEther, Wallet } from 'ethers' -import sample from 'lodash/sample' +import { formatEther, Wallet } from 'ethers' import { Plugin } from '../../Plugin' import PLUGIN_CONFIG_SCHEMA from './config.schema.json' +import { adjustStakes } from './payoutProportionalStrategy' +import { Action, SponsorshipId, SponsorshipState } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string @@ -14,10 +15,24 @@ export interface AutostakerPluginConfig { runIntervalInMs: number } -const STAKE_AMOUNT: WeiAmount = parseEther('10000') - const logger = new Logger(module) +const getStakeOrUnstakeFunction = (action: Action): ( + operatorOwnerWallet: SignerWithProvider, + operatorContractAddress: string, + sponsorshipContractAddress: string, + amount: WeiAmount +) => Promise => { + switch (action.type) { + case 'stake': + return _operatorContractUtils.stake + case 'unstake': + return _operatorContractUtils.unstake + default: + throw new Error('assertion failed') + } +} + export class AutostakerPlugin extends Plugin { private abortController: AbortController = new AbortController() @@ -38,11 +53,41 @@ export class AutostakerPlugin extends Plugin { const provider = (await streamrClient.getSigner()).provider const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) .connect(provider) - const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() - const availableBalance = (await operatorContract.valueWithoutEarnings()) - stakedAmount - logger.info(`Available balance: ${formatEther(availableBalance)} (staked=${formatEther(stakedAmount)})`) + const stakedWei = await operatorContract.totalStakedIntoSponsorshipsWei() + const unstakedWei = (await operatorContract.valueWithoutEarnings()) - stakedWei + logger.info(`Balance: unstaked=${formatEther(unstakedWei)}, staked=${formatEther(stakedWei)}`) + const stakeableSponsorships = await this.getStakeableSponsorships(streamrClient) + logger.info(`Stakeable sponsorships: ${[...stakeableSponsorships.keys()].join(',')}`) + const stakes = await this.getStakes(streamrClient) + const stakeDescription = [...stakes.entries()].map(([sponsorshipId, amountWei]) => `${sponsorshipId}=${formatEther(amountWei)}`).join(', ') + logger.info(`Stakes before adjustments: ${stakeDescription}`) + const actions = adjustStakes({ + operatorState: { + stakes, + unstakedWei + }, + operatorConfig: {}, // TODO add maxSponsorshipCount + stakeableSponsorships, + environmentConfig: { + minimumStakeWei: 5000000000000000000000n // TODO read from The Graph (network.minimumStakeWei) + } + }) + const operatorOwnerWallet = new Wallet(this.pluginConfig.operatorOwnerPrivateKey, provider) as SignerWithProvider + for (const action of actions) { + logger.info(`Action: ${action.type} ${formatEther(action.amount)} ${action.sponsorshipId}`) + await getStakeOrUnstakeFunction(action)(operatorOwnerWallet, + this.pluginConfig.operatorContractAddress, + action.sponsorshipId, + action.amount + ) + } + } + + // eslint-disable-next-line class-methods-use-this + private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { // TODO is there a better way to get the client? Maybe we should add StreamrClient#getTheGraphClient() - // TODO what are good where consitions for the sponsorships query + // TODO what are good where conditions for the sponsorships query so that we get all stakeable sponsorships + // but no non-stakables (e.g. expired) // @ts-expect-error private const queryResult = streamrClient.theGraphClient.queryEntities((lastId: string, pageSize: number) => { return { @@ -57,23 +102,48 @@ export class AutostakerPlugin extends Plugin { first: ${pageSize} ) { id + totalPayoutWeiPerSec + totalStakedWei } } ` } }) - const sponsorships: { id: string }[] = await collect(queryResult) - logger.info(`Available sponsorships: ${sponsorships.map((s) => s.id).join(',')}`) - if ((sponsorships.length) > 0 && (availableBalance >= STAKE_AMOUNT)) { - const targetSponsorship = sample(sponsorships)! - logger.info(`Stake ${formatEther(STAKE_AMOUNT)} to ${targetSponsorship.id}`) - await _operatorContractUtils.stake( - new Wallet(this.pluginConfig.operatorOwnerPrivateKey, provider) as SignerWithProvider, - this.pluginConfig.operatorContractAddress, - targetSponsorship.id, - STAKE_AMOUNT - ) - } + const sponsorships: { id: SponsorshipId, totalPayoutWeiPerSec: bigint, totalStakedWei: bigint }[] = await collect(queryResult) + return new Map(sponsorships.map( + (sponsorship) => [sponsorship.id, { + totalPayoutWeiPerSec: BigInt(sponsorship.totalPayoutWeiPerSec), + totalStakedWei: BigInt(sponsorship.totalStakedWei) + }]) + ) + } + + private async getStakes(streamrClient: StreamrClient): Promise> { + // TODO use StreamrClient#getTheGraphClient() + // @ts-expect-error private + const queryResult = streamrClient.theGraphClient.queryEntities((lastId: string, pageSize: number) => { + return { + query: ` + { + stakes( + where: { + operator: "${this.pluginConfig.operatorContractAddress.toLowerCase()}", + id_gt: "${lastId}" + }, + first: ${pageSize} + ) { + id + sponsorship { + id + } + amountWei + } + } + ` + } + }) + const stakes: { sponsorship: { id: SponsorshipId }, amountWei: bigint }[] = await collect(queryResult) + return new Map(stakes.map((stake) => [stake.sponsorship.id, BigInt(stake.amountWei) ])) } async stop(): Promise { diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts new file mode 100644 index 0000000000..ce3c719813 --- /dev/null +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -0,0 +1,113 @@ +import { sum } from '@streamr/utils' +import partition from 'lodash/partition' +import { Action, AdjustStakesFn } from './types' + +/** + * Allocate stake in proportion to the payout each sponsorship gives. + * Detailed allocation formula: each sponsorship should get the stake M + P / T, + * where M is the environment-mandated minimum stake, + * P is `totalPayoutWeiPerSec` of the considered sponsorship, and + * T is sum of `totalPayoutWeiPerSec` of all sponsorships this operator stakes to. + * `totalPayoutWeiPerSec` is the total payout per second to all staked operators. + * + * In order to prevent that operators' actions cause other operators to change their plans, + * this strategy only takes into account the payout, not what others have staked. + * + * In order to also prevent too much churn (joining and leaving), + * this strategy will keep those sponsorships that they already have staked to, as long as they keep paying. + * Eventually sponsorships expire, and then the strategy will reallocate that stake elsewhere. + * + * This strategy tries to stake to as many sponsorships as it can afford. + * Since smaller amounts than the minimum stake can't be staked, + * the tokens available to the operator limits the number of sponsorships they can stake to. + * + * Example: + * - there are sponsorships that pay out 2, 4, and 6 DATA/sec respectively. + * - minimum stake is 5000 DATA + * - operator has 11000 DATA (tokens in the contract plus currently staked tokens) + * 1. it only has enough for 2 sponsorships, so it chooses the ones that pay 4 and 6 DATA/sec + * 2. it allocates 5000 DATA to each of the 2 sponsorships (minimum stake) + * 3. it allocates the remaining 1000 DATA in proportion to the payout: + * - 1000 * 4 / 10 = 400 DATA to the 4-paying sponsorship + * - 1000 * 6 / 10 = 600 DATA to the 6-paying sponsorship + * - final target stakes are: 0, 5400, and 5600 DATA to the three sponsorships + * - the algorithm then outputs the stake and unstake actions to change the allocation to the target + **/ +export const adjustStakes: AdjustStakesFn = ({ + operatorState, + operatorConfig, + stakeableSponsorships, + environmentConfig +}): Action[] => { + const { unstakedWei, stakes } = operatorState + const { minimumStakeWei } = environmentConfig + const totalStakeableWei = sum([...stakes.values()]) + unstakedWei + + if (Array.from(stakeableSponsorships.values()).find(({ totalPayoutWeiPerSec }) => totalPayoutWeiPerSec <= 0n)) { + throw new Error('payoutProportional: sponsorships must have positive totalPayoutWeiPerSec') + } + + // find the number of sponsorships that we can afford to stake to + const targetSponsorshipCount = Math.min( + stakeableSponsorships.size, + operatorConfig.maxSponsorshipCount ?? Infinity, + Math.floor(Number(totalStakeableWei) / Number(minimumStakeWei)), + ) + + const sponsorshipList = Array.from(stakeableSponsorships.entries()).map( + ([id, { totalPayoutWeiPerSec }]) => ({ id, totalPayoutWeiPerSec }) + ) + + // separate the stakeable sponsorships that we already have stakes in + const [ + keptSponsorships, + potentialSponsorships, + ] = partition(sponsorshipList, ({ id }) => stakes.has(id)) + + // TODO: add secondary sorting of potentialSponsorships based on operator ID + sponsorship ID + // idea is: in case of a tie, operators should stake to different sponsorships + + // pad the kept sponsorships to the target number, in the order of decreasing payout + potentialSponsorships.sort((a, b) => Number(b.totalPayoutWeiPerSec) - Number(a.totalPayoutWeiPerSec)) + const selectedSponsorships = keptSponsorships + .concat(potentialSponsorships) + .slice(0, targetSponsorshipCount) + + // calculate the target stakes for each sponsorship: minimum stake plus payout-proportional allocation + const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei + const payoutProportionalWei = totalStakeableWei - minimumStakesWei + const payoutSumWeiPerSec = sum(selectedSponsorships.map(({ totalPayoutWeiPerSec }) => totalPayoutWeiPerSec)) + + const targetStakes = new Map(payoutSumWeiPerSec > 0n ? selectedSponsorships.map(({ id, totalPayoutWeiPerSec }) => + [id, minimumStakeWei + payoutProportionalWei * totalPayoutWeiPerSec / payoutSumWeiPerSec]) : []) + + // calculate the stake differences for all sponsorships we have stakes in, or want to stake into + const sponsorshipIdList = Array.from(new Set([...stakes.keys(), ...targetStakes.keys()])) + const differencesWei = sponsorshipIdList.map((sponsorshipId) => ({ + sponsorshipId, + differenceWei: (targetStakes.get(sponsorshipId) ?? 0n) - (stakes.get(sponsorshipId) ?? 0n) + })).filter(({ differenceWei: difference }) => difference !== 0n) + + // TODO: filter out too small (TODO: decide what "too small" means) stakings and unstakings because those just waste gas + + // sort the differences in ascending order (unstakings first, then stakings) + differencesWei.sort((a, b) => Number(a.differenceWei) - Number(b.differenceWei)) + + // force the net staking to equal unstakedWei (fixes e.g. rounding errors) by adjusting the largest staking (last in list) + const netStakingWei = sum(differencesWei.map(({ differenceWei: difference }) => difference)) + if (netStakingWei !== unstakedWei && sponsorshipList.length > 0 && differencesWei.length > 0) { + const largestDifference = differencesWei.pop()! + largestDifference.differenceWei += unstakedWei - netStakingWei + // don't push back a zero difference + if (largestDifference.differenceWei !== 0n) { + differencesWei.push(largestDifference) + } + } + + // convert differences to actions + return differencesWei.map(({ sponsorshipId, differenceWei }) => ({ + type: differenceWei > 0n ? 'stake' : 'unstake', + sponsorshipId, + amount: differenceWei > 0n ? differenceWei : -differenceWei + })) +} diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts new file mode 100644 index 0000000000..01441cb35b --- /dev/null +++ b/packages/node/src/plugins/autostaker/types.ts @@ -0,0 +1,46 @@ +export type SponsorshipId = string + +/** + * Actions that should be executed by the operator + */ +export interface Action { + type: 'stake' | 'unstake' + sponsorshipId: SponsorshipId + amount: bigint +} + +export type AdjustStakesFn = (opts: { + operatorState: OperatorState + operatorConfig: OperatorConfig + stakeableSponsorships: Map + environmentConfig: EnvironmentConfig +}) => Action[] + +/** + * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) + **/ +export interface SponsorshipState { + totalPayoutWeiPerSec: bigint + totalStakedWei: bigint +} + +/** + * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) + **/ +export interface OperatorState { + stakes: Map + unstakedWei: bigint +} + +export interface OperatorConfig { + maxSponsorshipCount?: number +} + +/** + * Network-wide constants, also available in thegraph + * @see https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L226 + */ +export interface EnvironmentConfig { + minimumStakeWei: bigint +} + diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts new file mode 100644 index 0000000000..924c83102c --- /dev/null +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -0,0 +1,206 @@ +import { adjustStakes } from '../../../../src/plugins/autostaker/payoutProportionalStrategy' + +describe('payoutProportionalStrategy', () => { + it('unstakes everything if no stakeable sponsorships', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 1234n, stakes: new Map([[ 'a', 1234n ]]) }, + operatorConfig: { }, + stakeableSponsorships: new Map(), + environmentConfig: { minimumStakeWei: 1234n }, + })).toEqual([ + { type: 'unstake', sponsorshipId: 'a', amount: 1234n }, + ]) + }) + + it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 600n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], + ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], + ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ + { type: 'stake', sponsorshipId: 'a', amount: 100n }, + { type: 'stake', sponsorshipId: 'b', amount: 200n }, + { type: 'stake', sponsorshipId: 'c', amount: 300n }, + ]) + }) + + it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 500n, stakes: new Map() }, + operatorConfig: { maxSponsorshipCount: 2 }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], // not included + ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], // included + ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], // included + ]), + environmentConfig: { minimumStakeWei: 0n }, + }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ + { type: 'stake', sponsorshipId: 'b', amount: 200n }, + { type: 'stake', sponsorshipId: 'c', amount: 300n }, + ]) + }) + + it('limits the targetSponsorshipCount to minimumStakeWei and available tokens', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 500n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], // not included + ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], // not included + ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], // included + ]), + environmentConfig: { minimumStakeWei: 300n }, + }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ + { type: 'stake', sponsorshipId: 'c', amount: 500n }, + ]) + }) + + it('doesn\'t allocate tokens if less available than minimum stake', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 100n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }]]), + environmentConfig: { minimumStakeWei: 300n }, + })).toEqual([]) + }) + + // unstakes must happen first because otherwise there isn't enough tokens for staking + it('sends out unstakes before stakes', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 0n, stakes: new Map([ + [ 'a', 30n ], + [ 'b', 70n ], + ]) }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 40n, totalStakedWei: 1000n }], // add stake here + ['b', { totalPayoutWeiPerSec: 30n, totalStakedWei: 1000n }], // unstake from here + ['c', { totalPayoutWeiPerSec: 20n, totalStakedWei: 1000n }], // stake here + ['d', { totalPayoutWeiPerSec: 10n, totalStakedWei: 1000n }], // stake here + ]), + environmentConfig: { minimumStakeWei: 0n }, + }).map((a) => a.type)).toEqual([ + 'unstake', + 'stake', + 'stake', + 'stake', + ]) + }) + + it('unstakes from expired sponsorships', async () => { + // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships + expect(adjustStakes({ + operatorState: { unstakedWei: 0n, stakes: new Map([[ 'b', 100n ]]) }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toEqual([ + { type: 'unstake', sponsorshipId: 'b', amount: 100n }, + { type: 'stake', sponsorshipId: 'a', amount: 100n }, + ]) + }) + + it('restakes expired sponsorship stakes into other sponsorships', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 0n, stakes: new Map([ + [ 'a', 100n ], + [ 'b', 100n ], + [ 'c', 100n ], + ]) }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 100n }], + ['b', { totalPayoutWeiPerSec: 10n, totalStakedWei: 100n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toEqual([ + { type: 'unstake', sponsorshipId: 'c', amount: 100n }, + { type: 'stake', sponsorshipId: 'a', amount: 50n }, + { type: 'stake', sponsorshipId: 'b', amount: 50n }, + ]) + }) + + it('handles rounding errors by adjusting the largest staking', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 100n, totalStakedWei: 0n }], + ['b', { totalPayoutWeiPerSec: 100n, totalStakedWei: 0n }], + ['c', { totalPayoutWeiPerSec: 400n, totalStakedWei: 0n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toEqual([ + { type: 'stake', sponsorshipId: 'a', amount: 166n }, + { type: 'stake', sponsorshipId: 'b', amount: 166n }, + { type: 'stake', sponsorshipId: 'c', amount: 668n }, + ]) + }) + + it('rounding error no-op case', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 0n, stakes: new Map([ + ['a', 166n ], + ['b', 166n ], + ['c', 668n ], + ]) }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 100n, totalStakedWei: 166n }], + ['b', { totalPayoutWeiPerSec: 100n, totalStakedWei: 166n }], + ['c', { totalPayoutWeiPerSec: 400n, totalStakedWei: 668n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toEqual([]) + }) + + it('uses Infinity as default maxSponsorshipCount', async () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], + ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], + ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], + ['d', { totalPayoutWeiPerSec: 40n, totalStakedWei: 0n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toHaveLength(4) + }) + + describe('input validation', () => { + it('throws if there is a sponsorship with totalPayoutWeiPerSec == 0', async () => { + expect(() => adjustStakes({ + operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], + ['b', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], + ['c', { totalPayoutWeiPerSec: 0n, totalStakedWei: 0n }], + ['d', { totalPayoutWeiPerSec: 0n, totalStakedWei: 0n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toThrow('payoutProportional: sponsorships must have positive totalPayoutWeiPerSec') + }) + it('throws if there is a sponsorship with totalPayoutWeiPerSec < 0', async () => { + expect(() => adjustStakes({ + operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], + ['b', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], + ['c', { totalPayoutWeiPerSec: -1n, totalStakedWei: 0n }], + ['d', { totalPayoutWeiPerSec: 234n, totalStakedWei: 0n }], + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toThrow('payoutProportional: sponsorships must have positive totalPayoutWeiPerSec') + }) + }) +}) diff --git a/packages/utils/src/exports.ts b/packages/utils/src/exports.ts index cbca783a42..d449928e4d 100644 --- a/packages/utils/src/exports.ts +++ b/packages/utils/src/exports.ts @@ -54,3 +54,4 @@ export type { ChangeFieldType, MapKey } from './types' export { type WeiAmount, multiplyWeiAmount } from './WeiAmount' export { getSubtle } from './crossPlatformCrypto' export { SigningUtil, EcdsaSecp256k1Evm, EcdsaSecp256r1, MlDsa87, type KeyType, KEY_TYPES } from './SigningUtil' +export { sum } from './sum' diff --git a/packages/utils/src/sum.ts b/packages/utils/src/sum.ts new file mode 100644 index 0000000000..b97cb710f6 --- /dev/null +++ b/packages/utils/src/sum.ts @@ -0,0 +1,3 @@ +export function sum(values: bigint[]): bigint { + return values.reduce((acc, value) => acc + value, 0n) +} From bd0d77b27675de1064a10f1ba0c7b0807931cb8e Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 19 May 2025 11:08:01 +0300 Subject: [PATCH 03/60] manual test: mulltiple sponsorships --- packages/node/bin/autostaker-manual-test.sh | 25 ++++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/node/bin/autostaker-manual-test.sh b/packages/node/bin/autostaker-manual-test.sh index cb2c147472..330ffd2255 100755 --- a/packages/node/bin/autostaker-manual-test.sh +++ b/packages/node/bin/autostaker-manual-test.sh @@ -3,25 +3,33 @@ NODE_PRIVATE_KEY="1111111111111111111111111111111111111111111111111111111111111111" OWNER_PRIVATE_KEY="2222222222222222222222222222222222222222222222222222222222222222" SPONSORER_PRIVATE_KEY="3333333333333333333333333333333333333333333333333333333333333333" -EARNINGS_PER_SECOND=12 -STAKED_AMOUNT=500000 -SPONSOR_AMOUNT=1234567 +EARNINGS_PER_SECOND_1=1000 +EARNINGS_PER_SECOND_2=2000 +DELEGATED_AMOUNT=500000 +SPONSOR_AMOUNT=600000 NODE_ADDRESS=$(ethereum-address $NODE_PRIVATE_KEY | jq -r '.address') OWNER_ADDRESS=$(ethereum-address $OWNER_PRIVATE_KEY | jq -r '.address') SPONSORER_ADDRESS=$(ethereum-address $SPONSORER_PRIVATE_KEY | jq -r '.address') cd ../../cli-tools + echo 'Mint tokens' npx tsx bin/streamr.ts internal token-mint $NODE_ADDRESS 10000000 10000000 --env dev2 npx tsx bin/streamr.ts internal token-mint $OWNER_ADDRESS 10000000 10000000 --env dev2 + echo 'Create operator' OPERATOR_CONTRACT_ADDRESS=$(npx tsx bin/streamr.ts internal operator-create -c 10 --node-addresses $NODE_ADDRESS --env dev2 --private-key $OWNER_PRIVATE_KEY | jq -r '.address') +npx tsx bin/streamr.ts internal operator-delegate $OPERATOR_CONTRACT_ADDRESS $DELEGATED_AMOUNT --env dev2 --private-key $OWNER_PRIVATE_KEY + +echo 'Create sponsorships' npx tsx bin/streamr.ts internal token-mint $SPONSORER_ADDRESS 10000000 10000000 --env dev2 -npx tsx bin/streamr.ts stream create /foo --env dev2 --private-key $SPONSORER_PRIVATE_KEY -SPONSORSHIP_CONTRACT_ADDRESS=$(npx tsx bin/streamr.ts internal sponsorship-create /foo -e $EARNINGS_PER_SECOND --env dev2 --private-key $SPONSORER_PRIVATE_KEY | jq -r '.address') -npx tsx bin/streamr.ts internal sponsorship-sponsor $SPONSORSHIP_CONTRACT_ADDRESS $SPONSOR_AMOUNT --env dev2 --private-key $SPONSORER_PRIVATE_KEY -npx tsx bin/streamr.ts internal operator-delegate $OPERATOR_CONTRACT_ADDRESS $STAKED_AMOUNT --env dev2 --private-key $OWNER_PRIVATE_KEY +npx tsx bin/streamr.ts stream create /foo1 --env dev2 --private-key $SPONSORER_PRIVATE_KEY +SPONSORSHIP_CONTRACT_ADDRESS_1=$(npx tsx bin/streamr.ts internal sponsorship-create /foo1 -e $EARNINGS_PER_SECOND_1 --env dev2 --private-key $SPONSORER_PRIVATE_KEY | jq -r '.address') +npx tsx bin/streamr.ts internal sponsorship-sponsor $SPONSORSHIP_CONTRACT_ADDRESS_1 $SPONSOR_AMOUNT --env dev2 --private-key $SPONSORER_PRIVATE_KEY +npx tsx bin/streamr.ts stream create /foo2 --env dev2 --private-key $SPONSORER_PRIVATE_KEY +SPONSORSHIP_CONTRACT_ADDRESS_2=$(npx tsx bin/streamr.ts internal sponsorship-create /foo2 -e $EARNINGS_PER_SECOND_2 --env dev2 --private-key $SPONSORER_PRIVATE_KEY | jq -r '.address') +npx tsx bin/streamr.ts internal sponsorship-sponsor $SPONSORSHIP_CONTRACT_ADDRESS_2 $SPONSOR_AMOUNT --env dev2 --private-key $SPONSORER_PRIVATE_KEY jq -n \ --arg nodePrivateKey "$NODE_PRIVATE_KEY" \ @@ -45,7 +53,8 @@ jq -n \ jq -n \ --arg operatorContract "$OPERATOR_CONTRACT_ADDRESS" \ - --arg sponsorshipContract "$SPONSORSHIP_CONTRACT_ADDRESS" \ + --arg sponsorshipContract1 "$SPONSORSHIP_CONTRACT_ADDRESS_1" \ + --arg sponsorshipContract2 "$SPONSORSHIP_CONTRACT_ADDRESS_2" \ '$ARGS.named' cd ../node From b6d76cf35bc8a8da03703a932f2778e8d53c6875 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Wed, 28 May 2025 12:10:40 +0300 Subject: [PATCH 04/60] use StreamrClient#getTheGraphClient() --- .../plugins/autostaker/AutostakerPlugin.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index af7c4a08ff..c698548117 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -15,6 +15,20 @@ export interface AutostakerPluginConfig { runIntervalInMs: number } +interface SponsorshipQueryResultItem { + id: SponsorshipId + totalPayoutWeiPerSec: bigint + totalStakedWei: bigint +} + +interface StakeQueryResultItem { + id: string + sponsorship: { + id: SponsorshipId + } + amountWei: bigint +} + const logger = new Logger(module) const getStakeOrUnstakeFunction = (action: Action): ( @@ -88,8 +102,7 @@ export class AutostakerPlugin extends Plugin { // TODO is there a better way to get the client? Maybe we should add StreamrClient#getTheGraphClient() // TODO what are good where conditions for the sponsorships query so that we get all stakeable sponsorships // but no non-stakables (e.g. expired) - // @ts-expect-error private - const queryResult = streamrClient.theGraphClient.queryEntities((lastId: string, pageSize: number) => { + const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { return { query: ` { @@ -109,7 +122,7 @@ export class AutostakerPlugin extends Plugin { ` } }) - const sponsorships: { id: SponsorshipId, totalPayoutWeiPerSec: bigint, totalStakedWei: bigint }[] = await collect(queryResult) + const sponsorships = await collect(queryResult) return new Map(sponsorships.map( (sponsorship) => [sponsorship.id, { totalPayoutWeiPerSec: BigInt(sponsorship.totalPayoutWeiPerSec), @@ -119,9 +132,7 @@ export class AutostakerPlugin extends Plugin { } private async getStakes(streamrClient: StreamrClient): Promise> { - // TODO use StreamrClient#getTheGraphClient() - // @ts-expect-error private - const queryResult = streamrClient.theGraphClient.queryEntities((lastId: string, pageSize: number) => { + const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { return { query: ` { @@ -142,7 +153,7 @@ export class AutostakerPlugin extends Plugin { ` } }) - const stakes: { sponsorship: { id: SponsorshipId }, amountWei: bigint }[] = await collect(queryResult) + const stakes = await collect(queryResult) return new Map(stakes.map((stake) => [stake.sponsorship.id, BigInt(stake.amountWei) ])) } From a61655652842f25e87e399c98216be324a8e1830 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Wed, 28 May 2025 12:28:23 +0300 Subject: [PATCH 05/60] cli-tools command --- ...reamr-internal-operator-grant-controller-role.ts | 13 +++++++++++++ packages/cli-tools/bin/streamr-internal.ts | 1 + packages/cli-tools/test/internal-operator.test.ts | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 packages/cli-tools/bin/streamr-internal-operator-grant-controller-role.ts diff --git a/packages/cli-tools/bin/streamr-internal-operator-grant-controller-role.ts b/packages/cli-tools/bin/streamr-internal-operator-grant-controller-role.ts new file mode 100644 index 0000000000..4efd68f572 --- /dev/null +++ b/packages/cli-tools/bin/streamr-internal-operator-grant-controller-role.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env node +import '../src/logLevel' + +import { StreamrClient, _operatorContractUtils } from '@streamr/sdk' +import { createClientCommand } from '../src/command' + +createClientCommand(async (client: StreamrClient, operatorContractAddress: string, userId: string) => { + const contract = _operatorContractUtils.getOperatorContract(operatorContractAddress).connect(await client.getSigner()) + await (await contract.grantRole(await contract.CONTROLLER_ROLE(), userId)).wait() +}) + .description('grant controller role to a user') + .arguments(' ') + .parseAsync() diff --git a/packages/cli-tools/bin/streamr-internal.ts b/packages/cli-tools/bin/streamr-internal.ts index 9b4b614301..758f89870a 100755 --- a/packages/cli-tools/bin/streamr-internal.ts +++ b/packages/cli-tools/bin/streamr-internal.ts @@ -16,5 +16,6 @@ program .command('operator-undelegate', 'undelegate funds from an operator') .command('operator-stake', 'stake operator\'s funds to a sponsorship') .command('operator-unstake', 'unstake all operator\'s funds from a sponsorship') + .command('operator-grant-controller-role', 'grant controller role to a user') .command('token-mint', 'mint test tokens') .parse() diff --git a/packages/cli-tools/test/internal-operator.test.ts b/packages/cli-tools/test/internal-operator.test.ts index 204659e2ca..6b58c88c13 100644 --- a/packages/cli-tools/test/internal-operator.test.ts +++ b/packages/cli-tools/test/internal-operator.test.ts @@ -52,6 +52,13 @@ describe('operator', () => { }) expect(await operatorContract.balanceInData(await delegator.getAddress())).toEqual(0n) + // grant controller role + const controller = await createTestWallet() + await runCommand(`internal operator-grant-controller-role ${operatorContractAddress} ${controller.address}`, { + privateKey: operator.privateKey + }) + expect(await operatorContract.hasRole(await operatorContract.CONTROLLER_ROLE(), controller.address)).toBeTrue() + await client.destroy() }, 30 * 1000) }) From 3e8bfcf40c5d7adbf3f3424ddcb78023d2582930 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Wed, 28 May 2025 12:29:30 +0300 Subject: [PATCH 06/60] grant controller role to the node --- packages/node/bin/autostaker-manual-test.sh | 3 +-- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 9 +++------ packages/node/src/plugins/autostaker/config.schema.json | 8 +------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/node/bin/autostaker-manual-test.sh b/packages/node/bin/autostaker-manual-test.sh index 330ffd2255..a405ab4331 100755 --- a/packages/node/bin/autostaker-manual-test.sh +++ b/packages/node/bin/autostaker-manual-test.sh @@ -21,6 +21,7 @@ npx tsx bin/streamr.ts internal token-mint $OWNER_ADDRESS 10000000 10000000 --en echo 'Create operator' OPERATOR_CONTRACT_ADDRESS=$(npx tsx bin/streamr.ts internal operator-create -c 10 --node-addresses $NODE_ADDRESS --env dev2 --private-key $OWNER_PRIVATE_KEY | jq -r '.address') npx tsx bin/streamr.ts internal operator-delegate $OPERATOR_CONTRACT_ADDRESS $DELEGATED_AMOUNT --env dev2 --private-key $OWNER_PRIVATE_KEY +npx tsx bin/streamr.ts internal operator-grant-controller-role $OPERATOR_CONTRACT_ADDRESS $NODE_ADDRESS --env dev2 --private-key $OWNER_PRIVATE_KEY echo 'Create sponsorships' npx tsx bin/streamr.ts internal token-mint $SPONSORER_ADDRESS 10000000 10000000 --env dev2 @@ -33,7 +34,6 @@ npx tsx bin/streamr.ts internal sponsorship-sponsor $SPONSORSHIP_CONTRACT_ADDRES jq -n \ --arg nodePrivateKey "$NODE_PRIVATE_KEY" \ - --arg operatorOwnerPrivateKey "$OWNER_PRIVATE_KEY" \ --arg operatorContractAddress "$OPERATOR_CONTRACT_ADDRESS" \ '{ "$schema": "https://schema.streamr.network/config-v3.schema.json", @@ -45,7 +45,6 @@ jq -n \ }, plugins: { autostaker: { - operatorOwnerPrivateKey: $operatorOwnerPrivateKey, operatorContractAddress: $operatorContractAddress } } diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index c698548117..b2aab55460 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -1,7 +1,7 @@ import { _operatorContractUtils, SignerWithProvider, StreamrClient } from '@streamr/sdk' import { collect, Logger, scheduleAtInterval, WeiAmount } from '@streamr/utils' import { Schema } from 'ajv' -import { formatEther, Wallet } from 'ethers' +import { formatEther } from 'ethers' import { Plugin } from '../../Plugin' import PLUGIN_CONFIG_SCHEMA from './config.schema.json' import { adjustStakes } from './payoutProportionalStrategy' @@ -9,9 +9,6 @@ import { Action, SponsorshipId, SponsorshipState } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string - // TODO is it possible implement this without exposing the private key here? - // e.g. by configuring so that operator nodes can stake behalf of the operator? - operatorOwnerPrivateKey: string runIntervalInMs: number } @@ -86,10 +83,10 @@ export class AutostakerPlugin extends Plugin { minimumStakeWei: 5000000000000000000000n // TODO read from The Graph (network.minimumStakeWei) } }) - const operatorOwnerWallet = new Wallet(this.pluginConfig.operatorOwnerPrivateKey, provider) as SignerWithProvider + const signer = await streamrClient.getSigner() for (const action of actions) { logger.info(`Action: ${action.type} ${formatEther(action.amount)} ${action.sponsorshipId}`) - await getStakeOrUnstakeFunction(action)(operatorOwnerWallet, + await getStakeOrUnstakeFunction(action)(signer, this.pluginConfig.operatorContractAddress, action.sponsorshipId, action.amount diff --git a/packages/node/src/plugins/autostaker/config.schema.json b/packages/node/src/plugins/autostaker/config.schema.json index f418d2e570..5234b11836 100644 --- a/packages/node/src/plugins/autostaker/config.schema.json +++ b/packages/node/src/plugins/autostaker/config.schema.json @@ -5,8 +5,7 @@ "description": "Autostaker plugin configuration", "additionalProperties": false, "required": [ - "operatorContractAddress", - "operatorOwnerPrivateKey" + "operatorContractAddress" ], "properties": { "operatorContractAddress": { @@ -14,11 +13,6 @@ "description": "Operator contract Ethereum address", "format": "ethereum-address" }, - "operatorOwnerPrivateKey": { - "type": "string", - "description": "Operator owner's private key", - "format": "ethereum-private-key" - }, "runIntervalInMs": { "type": "integer", "description": "The interval (in milliseconds) at which autostaking possibilities are analyzed and executed", From 49f673d045a0c163daf4b4caf0da37ac3e394d5e Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 07/60] rm guard --- .../autostaker/payoutProportionalStrategy.ts | 4 --- .../payoutProportionalStrategy.test.ts | 29 ------------------- 2 files changed, 33 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index ce3c719813..2a3930592c 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -43,10 +43,6 @@ export const adjustStakes: AdjustStakesFn = ({ const { minimumStakeWei } = environmentConfig const totalStakeableWei = sum([...stakes.values()]) + unstakedWei - if (Array.from(stakeableSponsorships.values()).find(({ totalPayoutWeiPerSec }) => totalPayoutWeiPerSec <= 0n)) { - throw new Error('payoutProportional: sponsorships must have positive totalPayoutWeiPerSec') - } - // find the number of sponsorships that we can afford to stake to const targetSponsorshipCount = Math.min( stakeableSponsorships.size, diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 924c83102c..666a7e3840 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -174,33 +174,4 @@ describe('payoutProportionalStrategy', () => { environmentConfig: { minimumStakeWei: 0n }, })).toHaveLength(4) }) - - describe('input validation', () => { - it('throws if there is a sponsorship with totalPayoutWeiPerSec == 0', async () => { - expect(() => adjustStakes({ - operatorState: { unstakedWei: 1000n, stakes: new Map() }, - operatorConfig: { }, - stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], - ['b', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], - ['c', { totalPayoutWeiPerSec: 0n, totalStakedWei: 0n }], - ['d', { totalPayoutWeiPerSec: 0n, totalStakedWei: 0n }], - ]), - environmentConfig: { minimumStakeWei: 0n }, - })).toThrow('payoutProportional: sponsorships must have positive totalPayoutWeiPerSec') - }) - it('throws if there is a sponsorship with totalPayoutWeiPerSec < 0', async () => { - expect(() => adjustStakes({ - operatorState: { unstakedWei: 1000n, stakes: new Map() }, - operatorConfig: { }, - stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], - ['b', { totalPayoutWeiPerSec: 123n, totalStakedWei: 0n }], - ['c', { totalPayoutWeiPerSec: -1n, totalStakedWei: 0n }], - ['d', { totalPayoutWeiPerSec: 234n, totalStakedWei: 0n }], - ]), - environmentConfig: { minimumStakeWei: 0n }, - })).toThrow('payoutProportional: sponsorships must have positive totalPayoutWeiPerSec') - }) - }) }) From f2a44c389eb9a866e743dea661f46c5baf5502bb Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 08/60] improve test --- .../plugins/autostaker/payoutProportionalStrategy.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 666a7e3840..48fae1c237 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -3,12 +3,12 @@ import { adjustStakes } from '../../../../src/plugins/autostaker/payoutProportio describe('payoutProportionalStrategy', () => { it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 1234n, stakes: new Map([[ 'a', 1234n ]]) }, + operatorState: { unstakedWei: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, operatorConfig: { }, stakeableSponsorships: new Map(), environmentConfig: { minimumStakeWei: 1234n }, })).toEqual([ - { type: 'unstake', sponsorshipId: 'a', amount: 1234n }, + { type: 'unstake', sponsorshipId: 'a', amount: 2000n }, ]) }) From c08fc53799f298bd421151a7776dc89b979293dd Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 09/60] simpler comparison --- .../node/src/plugins/autostaker/payoutProportionalStrategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 2a3930592c..1ffa174963 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -91,7 +91,7 @@ export const adjustStakes: AdjustStakesFn = ({ // force the net staking to equal unstakedWei (fixes e.g. rounding errors) by adjusting the largest staking (last in list) const netStakingWei = sum(differencesWei.map(({ differenceWei: difference }) => difference)) - if (netStakingWei !== unstakedWei && sponsorshipList.length > 0 && differencesWei.length > 0) { + if (netStakingWei !== unstakedWei && stakeableSponsorships.size > 0 && differencesWei.length > 0) { const largestDifference = differencesWei.pop()! largestDifference.differenceWei += unstakedWei - netStakingWei // don't push back a zero difference From 4f6488fe2e23f2c229a2cbf9a95dd0425b18f513 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 10/60] refactor: getTargetStakes() --- .../autostaker/payoutProportionalStrategy.ts | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 1ffa174963..5551e1acb7 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,6 +1,6 @@ import { sum } from '@streamr/utils' import partition from 'lodash/partition' -import { Action, AdjustStakesFn } from './types' +import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorState, SponsorshipId, SponsorshipState } from './types' /** * Allocate stake in proportion to the payout each sponsorship gives. @@ -33,21 +33,20 @@ import { Action, AdjustStakesFn } from './types' * - final target stakes are: 0, 5400, and 5600 DATA to the three sponsorships * - the algorithm then outputs the stake and unstake actions to change the allocation to the target **/ -export const adjustStakes: AdjustStakesFn = ({ - operatorState, - operatorConfig, - stakeableSponsorships, - environmentConfig -}): Action[] => { - const { unstakedWei, stakes } = operatorState - const { minimumStakeWei } = environmentConfig - const totalStakeableWei = sum([...stakes.values()]) + unstakedWei +const getTargetStakes = ( + operatorState: OperatorState, + operatorConfig: OperatorConfig, + stakeableSponsorships: Map, + environmentConfig: EnvironmentConfig +): Map => { + + const totalStakeableWei = sum([...operatorState.stakes.values()]) + operatorState.unstakedWei // find the number of sponsorships that we can afford to stake to const targetSponsorshipCount = Math.min( stakeableSponsorships.size, operatorConfig.maxSponsorshipCount ?? Infinity, - Math.floor(Number(totalStakeableWei) / Number(minimumStakeWei)), + Math.floor(Number(totalStakeableWei) / Number(environmentConfig.minimumStakeWei)), ) const sponsorshipList = Array.from(stakeableSponsorships.entries()).map( @@ -58,7 +57,7 @@ export const adjustStakes: AdjustStakesFn = ({ const [ keptSponsorships, potentialSponsorships, - ] = partition(sponsorshipList, ({ id }) => stakes.has(id)) + ] = partition(sponsorshipList, ({ id }) => operatorState.stakes.has(id)) // TODO: add secondary sorting of potentialSponsorships based on operator ID + sponsorship ID // idea is: in case of a tie, operators should stake to different sponsorships @@ -70,18 +69,28 @@ export const adjustStakes: AdjustStakesFn = ({ .slice(0, targetSponsorshipCount) // calculate the target stakes for each sponsorship: minimum stake plus payout-proportional allocation - const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei + const minimumStakesWei = BigInt(selectedSponsorships.length) * environmentConfig.minimumStakeWei const payoutProportionalWei = totalStakeableWei - minimumStakesWei const payoutSumWeiPerSec = sum(selectedSponsorships.map(({ totalPayoutWeiPerSec }) => totalPayoutWeiPerSec)) - const targetStakes = new Map(payoutSumWeiPerSec > 0n ? selectedSponsorships.map(({ id, totalPayoutWeiPerSec }) => - [id, minimumStakeWei + payoutProportionalWei * totalPayoutWeiPerSec / payoutSumWeiPerSec]) : []) + return new Map(payoutSumWeiPerSec > 0n ? selectedSponsorships.map(({ id, totalPayoutWeiPerSec }) => + [id, environmentConfig.minimumStakeWei + payoutProportionalWei * totalPayoutWeiPerSec / payoutSumWeiPerSec]) : []) +} + +export const adjustStakes: AdjustStakesFn = ({ + operatorState, + operatorConfig, + stakeableSponsorships, + environmentConfig +}): Action[] => { + + const targetStakes = getTargetStakes(operatorState, operatorConfig, stakeableSponsorships, environmentConfig) // calculate the stake differences for all sponsorships we have stakes in, or want to stake into - const sponsorshipIdList = Array.from(new Set([...stakes.keys(), ...targetStakes.keys()])) + const sponsorshipIdList = Array.from(new Set([...operatorState.stakes.keys(), ...targetStakes.keys()])) const differencesWei = sponsorshipIdList.map((sponsorshipId) => ({ sponsorshipId, - differenceWei: (targetStakes.get(sponsorshipId) ?? 0n) - (stakes.get(sponsorshipId) ?? 0n) + differenceWei: (targetStakes.get(sponsorshipId) ?? 0n) - (operatorState.stakes.get(sponsorshipId) ?? 0n) })).filter(({ differenceWei: difference }) => difference !== 0n) // TODO: filter out too small (TODO: decide what "too small" means) stakings and unstakings because those just waste gas @@ -91,9 +100,9 @@ export const adjustStakes: AdjustStakesFn = ({ // force the net staking to equal unstakedWei (fixes e.g. rounding errors) by adjusting the largest staking (last in list) const netStakingWei = sum(differencesWei.map(({ differenceWei: difference }) => difference)) - if (netStakingWei !== unstakedWei && stakeableSponsorships.size > 0 && differencesWei.length > 0) { + if (netStakingWei !== operatorState.unstakedWei && stakeableSponsorships.size > 0 && differencesWei.length > 0) { const largestDifference = differencesWei.pop()! - largestDifference.differenceWei += unstakedWei - netStakingWei + largestDifference.differenceWei += operatorState.unstakedWei - netStakingWei // don't push back a zero difference if (largestDifference.differenceWei !== 0n) { differencesWei.push(largestDifference) From 4159ba200d8a67af520c448799717a1606db961d Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 11/60] rm obsolete totalStakedWei field --- .../plugins/autostaker/AutostakerPlugin.ts | 3 -- packages/node/src/plugins/autostaker/types.ts | 1 - .../payoutProportionalStrategy.test.ts | 54 +++++++++---------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index b2aab55460..e55ccbd7df 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -15,7 +15,6 @@ export interface AutostakerPluginConfig { interface SponsorshipQueryResultItem { id: SponsorshipId totalPayoutWeiPerSec: bigint - totalStakedWei: bigint } interface StakeQueryResultItem { @@ -113,7 +112,6 @@ export class AutostakerPlugin extends Plugin { ) { id totalPayoutWeiPerSec - totalStakedWei } } ` @@ -123,7 +121,6 @@ export class AutostakerPlugin extends Plugin { return new Map(sponsorships.map( (sponsorship) => [sponsorship.id, { totalPayoutWeiPerSec: BigInt(sponsorship.totalPayoutWeiPerSec), - totalStakedWei: BigInt(sponsorship.totalStakedWei) }]) ) } diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 01441cb35b..feb7eac218 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -21,7 +21,6 @@ export type AdjustStakesFn = (opts: { **/ export interface SponsorshipState { totalPayoutWeiPerSec: bigint - totalStakedWei: bigint } /** diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 48fae1c237..7a29915c87 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -17,9 +17,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 600n, stakes: new Map() }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], - ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], - ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], + ['a', { totalPayoutWeiPerSec: 10n }], + ['b', { totalPayoutWeiPerSec: 20n }], + ['c', { totalPayoutWeiPerSec: 30n }], ]), environmentConfig: { minimumStakeWei: 0n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -34,9 +34,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 500n, stakes: new Map() }, operatorConfig: { maxSponsorshipCount: 2 }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], // not included - ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], // included - ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], // included + ['a', { totalPayoutWeiPerSec: 10n }], // not included + ['b', { totalPayoutWeiPerSec: 20n }], // included + ['c', { totalPayoutWeiPerSec: 30n }], // included ]), environmentConfig: { minimumStakeWei: 0n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -50,9 +50,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 500n, stakes: new Map() }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], // not included - ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], // not included - ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], // included + ['a', { totalPayoutWeiPerSec: 10n }], // not included + ['b', { totalPayoutWeiPerSec: 20n }], // not included + ['c', { totalPayoutWeiPerSec: 30n }], // included ]), environmentConfig: { minimumStakeWei: 300n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -64,7 +64,7 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ operatorState: { unstakedWei: 100n, stakes: new Map() }, operatorConfig: { }, - stakeableSponsorships: new Map([['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }]]), + stakeableSponsorships: new Map([['a', { totalPayoutWeiPerSec: 10n }]]), environmentConfig: { minimumStakeWei: 300n }, })).toEqual([]) }) @@ -78,10 +78,10 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 40n, totalStakedWei: 1000n }], // add stake here - ['b', { totalPayoutWeiPerSec: 30n, totalStakedWei: 1000n }], // unstake from here - ['c', { totalPayoutWeiPerSec: 20n, totalStakedWei: 1000n }], // stake here - ['d', { totalPayoutWeiPerSec: 10n, totalStakedWei: 1000n }], // stake here + ['a', { totalPayoutWeiPerSec: 40n }], // add stake here + ['b', { totalPayoutWeiPerSec: 30n }], // unstake from here + ['c', { totalPayoutWeiPerSec: 20n }], // stake here + ['d', { totalPayoutWeiPerSec: 10n }], // stake here ]), environmentConfig: { minimumStakeWei: 0n }, }).map((a) => a.type)).toEqual([ @@ -98,7 +98,7 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 0n, stakes: new Map([[ 'b', 100n ]]) }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], + ['a', { totalPayoutWeiPerSec: 10n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toEqual([ @@ -116,8 +116,8 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 100n }], - ['b', { totalPayoutWeiPerSec: 10n, totalStakedWei: 100n }], + ['a', { totalPayoutWeiPerSec: 10n }], + ['b', { totalPayoutWeiPerSec: 10n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toEqual([ @@ -132,9 +132,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 1000n, stakes: new Map() }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n, totalStakedWei: 0n }], - ['b', { totalPayoutWeiPerSec: 100n, totalStakedWei: 0n }], - ['c', { totalPayoutWeiPerSec: 400n, totalStakedWei: 0n }], + ['a', { totalPayoutWeiPerSec: 100n }], + ['b', { totalPayoutWeiPerSec: 100n }], + ['c', { totalPayoutWeiPerSec: 400n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toEqual([ @@ -153,9 +153,9 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n, totalStakedWei: 166n }], - ['b', { totalPayoutWeiPerSec: 100n, totalStakedWei: 166n }], - ['c', { totalPayoutWeiPerSec: 400n, totalStakedWei: 668n }], + ['a', { totalPayoutWeiPerSec: 100n }], + ['b', { totalPayoutWeiPerSec: 100n }], + ['c', { totalPayoutWeiPerSec: 400n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toEqual([]) @@ -166,10 +166,10 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 1000n, stakes: new Map() }, operatorConfig: { }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n, totalStakedWei: 0n }], - ['b', { totalPayoutWeiPerSec: 20n, totalStakedWei: 0n }], - ['c', { totalPayoutWeiPerSec: 30n, totalStakedWei: 0n }], - ['d', { totalPayoutWeiPerSec: 40n, totalStakedWei: 0n }], + ['a', { totalPayoutWeiPerSec: 10n }], + ['b', { totalPayoutWeiPerSec: 20n }], + ['c', { totalPayoutWeiPerSec: 30n }], + ['d', { totalPayoutWeiPerSec: 40n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toHaveLength(4) From 6fcc2e9c0e47cd8f407142ef4bd1d7b06d219746 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 12/60] rename SponsorshipId --- .../node/src/plugins/autostaker/AutostakerPlugin.ts | 10 +++++----- .../plugins/autostaker/payoutProportionalStrategy.ts | 6 +++--- packages/node/src/plugins/autostaker/types.ts | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index e55ccbd7df..12144e611f 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -5,7 +5,7 @@ import { formatEther } from 'ethers' import { Plugin } from '../../Plugin' import PLUGIN_CONFIG_SCHEMA from './config.schema.json' import { adjustStakes } from './payoutProportionalStrategy' -import { Action, SponsorshipId, SponsorshipState } from './types' +import { Action, SponsorshipID, SponsorshipState } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string @@ -13,14 +13,14 @@ export interface AutostakerPluginConfig { } interface SponsorshipQueryResultItem { - id: SponsorshipId + id: SponsorshipID totalPayoutWeiPerSec: bigint } interface StakeQueryResultItem { id: string sponsorship: { - id: SponsorshipId + id: SponsorshipID } amountWei: bigint } @@ -94,7 +94,7 @@ export class AutostakerPlugin extends Plugin { } // eslint-disable-next-line class-methods-use-this - private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { + private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { // TODO is there a better way to get the client? Maybe we should add StreamrClient#getTheGraphClient() // TODO what are good where conditions for the sponsorships query so that we get all stakeable sponsorships // but no non-stakables (e.g. expired) @@ -125,7 +125,7 @@ export class AutostakerPlugin extends Plugin { ) } - private async getStakes(streamrClient: StreamrClient): Promise> { + private async getStakes(streamrClient: StreamrClient): Promise> { const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { return { query: ` diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 5551e1acb7..0418e4120c 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,6 +1,6 @@ import { sum } from '@streamr/utils' import partition from 'lodash/partition' -import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorState, SponsorshipId, SponsorshipState } from './types' +import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorState, SponsorshipID, SponsorshipState } from './types' /** * Allocate stake in proportion to the payout each sponsorship gives. @@ -37,9 +37,9 @@ import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorStat const getTargetStakes = ( operatorState: OperatorState, operatorConfig: OperatorConfig, - stakeableSponsorships: Map, + stakeableSponsorships: Map, environmentConfig: EnvironmentConfig -): Map => { +): Map => { const totalStakeableWei = sum([...operatorState.stakes.values()]) + operatorState.unstakedWei // find the number of sponsorships that we can afford to stake to diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index feb7eac218..3c686af996 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -1,18 +1,18 @@ -export type SponsorshipId = string +export type SponsorshipID = string /** * Actions that should be executed by the operator */ export interface Action { type: 'stake' | 'unstake' - sponsorshipId: SponsorshipId + sponsorshipId: SponsorshipID amount: bigint } export type AdjustStakesFn = (opts: { operatorState: OperatorState operatorConfig: OperatorConfig - stakeableSponsorships: Map + stakeableSponsorships: Map environmentConfig: EnvironmentConfig }) => Action[] @@ -27,7 +27,7 @@ export interface SponsorshipState { * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) **/ export interface OperatorState { - stakes: Map + stakes: Map unstakedWei: bigint } From d8a21569b3be6b6016ec7e02920595c583c1fa4c Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 13/60] use WeiAmount type --- .../node/src/plugins/autostaker/AutostakerPlugin.ts | 6 +++--- .../plugins/autostaker/payoutProportionalStrategy.ts | 4 ++-- packages/node/src/plugins/autostaker/types.ts | 12 +++++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 12144e611f..acc2f00ad4 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -14,7 +14,7 @@ export interface AutostakerPluginConfig { interface SponsorshipQueryResultItem { id: SponsorshipID - totalPayoutWeiPerSec: bigint + totalPayoutWeiPerSec: WeiAmount } interface StakeQueryResultItem { @@ -22,7 +22,7 @@ interface StakeQueryResultItem { sponsorship: { id: SponsorshipID } - amountWei: bigint + amountWei: WeiAmount } const logger = new Logger(module) @@ -125,7 +125,7 @@ export class AutostakerPlugin extends Plugin { ) } - private async getStakes(streamrClient: StreamrClient): Promise> { + private async getStakes(streamrClient: StreamrClient): Promise> { const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { return { query: ` diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 0418e4120c..02cf42189c 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,4 +1,4 @@ -import { sum } from '@streamr/utils' +import { sum, WeiAmount } from '@streamr/utils' import partition from 'lodash/partition' import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorState, SponsorshipID, SponsorshipState } from './types' @@ -39,7 +39,7 @@ const getTargetStakes = ( operatorConfig: OperatorConfig, stakeableSponsorships: Map, environmentConfig: EnvironmentConfig -): Map => { +): Map => { const totalStakeableWei = sum([...operatorState.stakes.values()]) + operatorState.unstakedWei // find the number of sponsorships that we can afford to stake to diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 3c686af996..e6874c2e24 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -1,3 +1,5 @@ +import { WeiAmount } from '@streamr/utils' + export type SponsorshipID = string /** @@ -6,7 +8,7 @@ export type SponsorshipID = string export interface Action { type: 'stake' | 'unstake' sponsorshipId: SponsorshipID - amount: bigint + amount: WeiAmount } export type AdjustStakesFn = (opts: { @@ -20,15 +22,15 @@ export type AdjustStakesFn = (opts: { * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) **/ export interface SponsorshipState { - totalPayoutWeiPerSec: bigint + totalPayoutWeiPerSec: WeiAmount } /** * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) **/ export interface OperatorState { - stakes: Map - unstakedWei: bigint + stakes: Map + unstakedWei: WeiAmount } export interface OperatorConfig { @@ -40,6 +42,6 @@ export interface OperatorConfig { * @see https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L226 */ export interface EnvironmentConfig { - minimumStakeWei: bigint + minimumStakeWei: WeiAmount } From 19a813412c2e8552b25609b287c5d50d2b6795eb Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 14/60] rename SponsorshipState --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 4 ++-- .../node/src/plugins/autostaker/payoutProportionalStrategy.ts | 4 ++-- packages/node/src/plugins/autostaker/types.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index acc2f00ad4..858510b348 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -5,7 +5,7 @@ import { formatEther } from 'ethers' import { Plugin } from '../../Plugin' import PLUGIN_CONFIG_SCHEMA from './config.schema.json' import { adjustStakes } from './payoutProportionalStrategy' -import { Action, SponsorshipID, SponsorshipState } from './types' +import { Action, SponsorshipID, SponsorshipConfig } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string @@ -94,7 +94,7 @@ export class AutostakerPlugin extends Plugin { } // eslint-disable-next-line class-methods-use-this - private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { + private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { // TODO is there a better way to get the client? Maybe we should add StreamrClient#getTheGraphClient() // TODO what are good where conditions for the sponsorships query so that we get all stakeable sponsorships // but no non-stakables (e.g. expired) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 02cf42189c..bac822cbe5 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,6 +1,6 @@ import { sum, WeiAmount } from '@streamr/utils' import partition from 'lodash/partition' -import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorState, SponsorshipID, SponsorshipState } from './types' +import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorState, SponsorshipID, SponsorshipConfig } from './types' /** * Allocate stake in proportion to the payout each sponsorship gives. @@ -37,7 +37,7 @@ import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorStat const getTargetStakes = ( operatorState: OperatorState, operatorConfig: OperatorConfig, - stakeableSponsorships: Map, + stakeableSponsorships: Map, environmentConfig: EnvironmentConfig ): Map => { diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index e6874c2e24..f76ecfc081 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -14,14 +14,14 @@ export interface Action { export type AdjustStakesFn = (opts: { operatorState: OperatorState operatorConfig: OperatorConfig - stakeableSponsorships: Map + stakeableSponsorships: Map environmentConfig: EnvironmentConfig }) => Action[] /** * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) **/ -export interface SponsorshipState { +export interface SponsorshipConfig { totalPayoutWeiPerSec: WeiAmount } From 8fc1e3e93b675a34952c3133ffaa107eecd34632 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 15/60] tests --- .../payoutProportionalStrategy.test.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 7a29915c87..84c2b56751 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -1,6 +1,23 @@ import { adjustStakes } from '../../../../src/plugins/autostaker/payoutProportionalStrategy' describe('payoutProportionalStrategy', () => { + + it('stake all', () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 11000n, stakes: new Map() }, + operatorConfig: { }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 2n }], + ['b', { totalPayoutWeiPerSec: 4n }], + ['c', { totalPayoutWeiPerSec: 6n }], + ]), + environmentConfig: { minimumStakeWei: 5000n }, + })).toIncludeSameMembers([ + { type: 'stake', sponsorshipId: 'b', amount: 5400n }, + { type: 'stake', sponsorshipId: 'c', amount: 5600n } + ]) + }) + it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, @@ -101,7 +118,7 @@ describe('payoutProportionalStrategy', () => { ['a', { totalPayoutWeiPerSec: 10n }], ]), environmentConfig: { minimumStakeWei: 0n }, - })).toEqual([ + })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'b', amount: 100n }, { type: 'stake', sponsorshipId: 'a', amount: 100n }, ]) @@ -120,14 +137,14 @@ describe('payoutProportionalStrategy', () => { ['b', { totalPayoutWeiPerSec: 10n }], ]), environmentConfig: { minimumStakeWei: 0n }, - })).toEqual([ + })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'c', amount: 100n }, { type: 'stake', sponsorshipId: 'a', amount: 50n }, { type: 'stake', sponsorshipId: 'b', amount: 50n }, ]) }) - it('handles rounding errors by adjusting the largest staking', async () => { + it('handles rounding errors', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map() }, operatorConfig: { }, @@ -137,7 +154,7 @@ describe('payoutProportionalStrategy', () => { ['c', { totalPayoutWeiPerSec: 400n }], ]), environmentConfig: { minimumStakeWei: 0n }, - })).toEqual([ + })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'a', amount: 166n }, { type: 'stake', sponsorshipId: 'b', amount: 166n }, { type: 'stake', sponsorshipId: 'c', amount: 668n }, From cf639ab3287310270950dde924c5bbf0f4462b3a Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 16/60] getSelectedSponsorships() --- .../autostaker/payoutProportionalStrategy.ts | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index bac822cbe5..49c3403ed5 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,6 +1,7 @@ import { sum, WeiAmount } from '@streamr/utils' import partition from 'lodash/partition' -import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorState, SponsorshipID, SponsorshipConfig } from './types' +import sortBy from 'lodash/sortBy' +import { Action, AdjustStakesFn, SponsorshipConfig, SponsorshipID } from './types' /** * Allocate stake in proportion to the payout each sponsorship gives. @@ -34,47 +35,51 @@ import { Action, AdjustStakesFn, EnvironmentConfig, OperatorConfig, OperatorStat * - the algorithm then outputs the stake and unstake actions to change the allocation to the target **/ -const getTargetStakes = ( - operatorState: OperatorState, - operatorConfig: OperatorConfig, +/* + * Select sponsorships for which we should have some stake + */ +const getSelectedSponsorships = ( + stakes: Map, stakeableSponsorships: Map, - environmentConfig: EnvironmentConfig -): Map => { - - const totalStakeableWei = sum([...operatorState.stakes.values()]) + operatorState.unstakedWei - // find the number of sponsorships that we can afford to stake to - const targetSponsorshipCount = Math.min( + totalStakeableWei: WeiAmount, + minimumStakeWei: WeiAmount, + maxSponsorshipCount: number | undefined +): SponsorshipID[] => { + const count = Math.min( stakeableSponsorships.size, - operatorConfig.maxSponsorshipCount ?? Infinity, - Math.floor(Number(totalStakeableWei) / Number(environmentConfig.minimumStakeWei)), + maxSponsorshipCount ?? Infinity, + Math.floor(Number(totalStakeableWei) / Number(minimumStakeWei)) // as many as we can afford ) - - const sponsorshipList = Array.from(stakeableSponsorships.entries()).map( - ([id, { totalPayoutWeiPerSec }]) => ({ id, totalPayoutWeiPerSec }) - ) - - // separate the stakeable sponsorships that we already have stakes in const [ keptSponsorships, potentialSponsorships, - ] = partition(sponsorshipList, ({ id }) => operatorState.stakes.has(id)) - - // TODO: add secondary sorting of potentialSponsorships based on operator ID + sponsorship ID - // idea is: in case of a tie, operators should stake to different sponsorships - - // pad the kept sponsorships to the target number, in the order of decreasing payout - potentialSponsorships.sort((a, b) => Number(b.totalPayoutWeiPerSec) - Number(a.totalPayoutWeiPerSec)) - const selectedSponsorships = keptSponsorships - .concat(potentialSponsorships) - .slice(0, targetSponsorshipCount) + ] = partition([...stakeableSponsorships.keys()], (id) => stakes.has(id)) + return [ + ...keptSponsorships, + // TODO: add secondary sorting of potentialSponsorships based on operator ID + sponsorship ID + // idea is: in case of a tie, operators should stake to different sponsorships + ...sortBy(potentialSponsorships, (id) => -Number(stakeableSponsorships.get(id)!.totalPayoutWeiPerSec)) + ].slice(0, count) +} - // calculate the target stakes for each sponsorship: minimum stake plus payout-proportional allocation - const minimumStakesWei = BigInt(selectedSponsorships.length) * environmentConfig.minimumStakeWei +/* + * Calculate the target stakes for each sponsorship: minimum stake plus payout-proportional allocation + */ +const getTargetStakes = ( + selectedSponsorships: SponsorshipID[], + stakeableSponsorships: Map, + totalStakeableWei: WeiAmount, + minimumStakeWei: WeiAmount +): Map => { + const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei const payoutProportionalWei = totalStakeableWei - minimumStakesWei - const payoutSumWeiPerSec = sum(selectedSponsorships.map(({ totalPayoutWeiPerSec }) => totalPayoutWeiPerSec)) - - return new Map(payoutSumWeiPerSec > 0n ? selectedSponsorships.map(({ id, totalPayoutWeiPerSec }) => - [id, environmentConfig.minimumStakeWei + payoutProportionalWei * totalPayoutWeiPerSec / payoutSumWeiPerSec]) : []) + const payoutSumWeiPerSec = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.totalPayoutWeiPerSec)) + return new Map( + selectedSponsorships.map((id) => [ + id, + minimumStakeWei + payoutProportionalWei * stakeableSponsorships.get(id)!.totalPayoutWeiPerSec / payoutSumWeiPerSec + ]) + ) } export const adjustStakes: AdjustStakesFn = ({ @@ -84,7 +89,20 @@ export const adjustStakes: AdjustStakesFn = ({ environmentConfig }): Action[] => { - const targetStakes = getTargetStakes(operatorState, operatorConfig, stakeableSponsorships, environmentConfig) + const totalStakeableWei = sum([...operatorState.stakes.values()]) + operatorState.unstakedWei + const selectedSponsorships = getSelectedSponsorships( + operatorState.stakes, + stakeableSponsorships, + totalStakeableWei, + environmentConfig.minimumStakeWei, + operatorConfig.maxSponsorshipCount + ) + const targetStakes = getTargetStakes( + selectedSponsorships, + stakeableSponsorships, + totalStakeableWei, + environmentConfig.minimumStakeWei + ) // calculate the stake differences for all sponsorships we have stakes in, or want to stake into const sponsorshipIdList = Array.from(new Set([...operatorState.stakes.keys(), ...targetStakes.keys()])) From a7e262b9de9f2c2f1f5c990098695b9005941206 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 17/60] getTargetStakes() includes expired sponsorships --- .../autostaker/payoutProportionalStrategy.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 49c3403ed5..3e36f23493 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -35,6 +35,11 @@ import { Action, AdjustStakesFn, SponsorshipConfig, SponsorshipID } from './type * - the algorithm then outputs the stake and unstake actions to change the allocation to the target **/ +type TargetStake = [SponsorshipID, WeiAmount] + +const getExpiredSponsorships = (stakes: Map, stakeableSponsorships: Map): SponsorshipID[] => { + return [...stakes.keys()].filter((sponsorshipId) => !stakeableSponsorships.has(sponsorshipId)) +} /* * Select sponsorships for which we should have some stake */ @@ -63,9 +68,12 @@ const getSelectedSponsorships = ( } /* - * Calculate the target stakes for each sponsorship: minimum stake plus payout-proportional allocation + * Calculate the target stakes for each sponsorship: + * - for selected sponsorships the stake is minimum stake plus payout-proportional allocation + * - for expired sponsorships the stake is zero */ const getTargetStakes = ( + stakes: Map, selectedSponsorships: SponsorshipID[], stakeableSponsorships: Map, totalStakeableWei: WeiAmount, @@ -74,12 +82,15 @@ const getTargetStakes = ( const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei const payoutProportionalWei = totalStakeableWei - minimumStakesWei const payoutSumWeiPerSec = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.totalPayoutWeiPerSec)) - return new Map( - selectedSponsorships.map((id) => [ - id, - minimumStakeWei + payoutProportionalWei * stakeableSponsorships.get(id)!.totalPayoutWeiPerSec / payoutSumWeiPerSec - ]) - ) + const targetsForSelected: TargetStake[] = selectedSponsorships.map((id) => [ + id, + minimumStakeWei + payoutProportionalWei * stakeableSponsorships.get(id)!.totalPayoutWeiPerSec / payoutSumWeiPerSec + ]) + const targetsForExpired: TargetStake[] = getExpiredSponsorships(stakes, stakeableSponsorships).map((id) => [ + id, + 0n + ]) + return new Map([...targetsForSelected, ...targetsForExpired]) } export const adjustStakes: AdjustStakesFn = ({ @@ -98,18 +109,16 @@ export const adjustStakes: AdjustStakesFn = ({ operatorConfig.maxSponsorshipCount ) const targetStakes = getTargetStakes( + operatorState.stakes, selectedSponsorships, stakeableSponsorships, totalStakeableWei, environmentConfig.minimumStakeWei ) - // calculate the stake differences for all sponsorships we have stakes in, or want to stake into - const sponsorshipIdList = Array.from(new Set([...operatorState.stakes.keys(), ...targetStakes.keys()])) - const differencesWei = sponsorshipIdList.map((sponsorshipId) => ({ - sponsorshipId, - differenceWei: (targetStakes.get(sponsorshipId) ?? 0n) - (operatorState.stakes.get(sponsorshipId) ?? 0n) - })).filter(({ differenceWei: difference }) => difference !== 0n) + const differencesWei = [...targetStakes.keys()] + .map((sponsorshipId) => ({ sponsorshipId, differenceWei: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) })) + .filter(({ differenceWei: difference }) => difference !== 0n) // TODO: filter out too small (TODO: decide what "too small" means) stakings and unstakings because those just waste gas From d3cd2dc78de1315b239dd4ef2a67ac8fc2077e8a Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:53 +0300 Subject: [PATCH 18/60] getTargetStakes() calls getSelectedSponsorships() --- .../autostaker/payoutProportionalStrategy.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 3e36f23493..8ca6effc2b 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -74,11 +74,19 @@ const getSelectedSponsorships = ( */ const getTargetStakes = ( stakes: Map, - selectedSponsorships: SponsorshipID[], stakeableSponsorships: Map, - totalStakeableWei: WeiAmount, - minimumStakeWei: WeiAmount + unstakedWei: WeiAmount, + minimumStakeWei: WeiAmount, + maxSponsorshipCount: number | undefined ): Map => { + const totalStakeableWei = sum([...stakes.values()]) + unstakedWei + const selectedSponsorships = getSelectedSponsorships( + stakes, + stakeableSponsorships, + totalStakeableWei, + minimumStakeWei, + maxSponsorshipCount + ) const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei const payoutProportionalWei = totalStakeableWei - minimumStakesWei const payoutSumWeiPerSec = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.totalPayoutWeiPerSec)) @@ -100,21 +108,13 @@ export const adjustStakes: AdjustStakesFn = ({ environmentConfig }): Action[] => { - const totalStakeableWei = sum([...operatorState.stakes.values()]) + operatorState.unstakedWei - const selectedSponsorships = getSelectedSponsorships( + const targetStakes = getTargetStakes( operatorState.stakes, stakeableSponsorships, - totalStakeableWei, + operatorState.unstakedWei, environmentConfig.minimumStakeWei, operatorConfig.maxSponsorshipCount ) - const targetStakes = getTargetStakes( - operatorState.stakes, - selectedSponsorships, - stakeableSponsorships, - totalStakeableWei, - environmentConfig.minimumStakeWei - ) const differencesWei = [...targetStakes.keys()] .map((sponsorshipId) => ({ sponsorshipId, differenceWei: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) })) From 17c1a04c57e98c6d4ffb44a9d9eeefd69df1af3e Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 19/60] sort only by adjustment type --- .../autostaker/payoutProportionalStrategy.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 8ca6effc2b..1e6899a331 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,5 +1,7 @@ import { sum, WeiAmount } from '@streamr/utils' import partition from 'lodash/partition' +import maxBy from 'lodash/maxBy' +import pull from 'lodash/pull' import sortBy from 'lodash/sortBy' import { Action, AdjustStakesFn, SponsorshipConfig, SponsorshipID } from './types' @@ -122,24 +124,22 @@ export const adjustStakes: AdjustStakesFn = ({ // TODO: filter out too small (TODO: decide what "too small" means) stakings and unstakings because those just waste gas - // sort the differences in ascending order (unstakings first, then stakings) - differencesWei.sort((a, b) => Number(a.differenceWei) - Number(b.differenceWei)) - - // force the net staking to equal unstakedWei (fixes e.g. rounding errors) by adjusting the largest staking (last in list) + // fix rounding errors by forcing the net staking to equal unstakedWei: adjust the largest staking const netStakingWei = sum(differencesWei.map(({ differenceWei: difference }) => difference)) if (netStakingWei !== operatorState.unstakedWei && stakeableSponsorships.size > 0 && differencesWei.length > 0) { - const largestDifference = differencesWei.pop()! + const largestDifference = maxBy(differencesWei, (d) => Number(d.differenceWei))! largestDifference.differenceWei += operatorState.unstakedWei - netStakingWei - // don't push back a zero difference - if (largestDifference.differenceWei !== 0n) { - differencesWei.push(largestDifference) + if (largestDifference.differenceWei === 0n) { + pull(differencesWei, largestDifference) } } - // convert differences to actions - return differencesWei.map(({ sponsorshipId, differenceWei }) => ({ - type: differenceWei > 0n ? 'stake' : 'unstake', - sponsorshipId, - amount: differenceWei > 0n ? differenceWei : -differenceWei - })) + return sortBy( + differencesWei.map(({ sponsorshipId, differenceWei }) => ({ + type: differenceWei > 0n ? 'stake' : 'unstake', + sponsorshipId, + amount: differenceWei > 0n ? differenceWei : -differenceWei + })), + (action) => ['unstake', 'stake'].indexOf(action.type) + ) } From 2b7b48250958a38b172fa794fb3c134f7480df0a Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 20/60] rename variable --- .../plugins/autostaker/payoutProportionalStrategy.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 1e6899a331..7275e48852 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -118,24 +118,24 @@ export const adjustStakes: AdjustStakesFn = ({ operatorConfig.maxSponsorshipCount ) - const differencesWei = [...targetStakes.keys()] + const adjustments = [...targetStakes.keys()] .map((sponsorshipId) => ({ sponsorshipId, differenceWei: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) })) .filter(({ differenceWei: difference }) => difference !== 0n) // TODO: filter out too small (TODO: decide what "too small" means) stakings and unstakings because those just waste gas // fix rounding errors by forcing the net staking to equal unstakedWei: adjust the largest staking - const netStakingWei = sum(differencesWei.map(({ differenceWei: difference }) => difference)) - if (netStakingWei !== operatorState.unstakedWei && stakeableSponsorships.size > 0 && differencesWei.length > 0) { - const largestDifference = maxBy(differencesWei, (d) => Number(d.differenceWei))! + const netStakingWei = sum(adjustments.map(({ differenceWei: difference }) => difference)) + if (netStakingWei !== operatorState.unstakedWei && stakeableSponsorships.size > 0 && adjustments.length > 0) { + const largestDifference = maxBy(adjustments, (a) => Number(d.differenceWei))! largestDifference.differenceWei += operatorState.unstakedWei - netStakingWei if (largestDifference.differenceWei === 0n) { - pull(differencesWei, largestDifference) + pull(adjustments, largestDifference) } } return sortBy( - differencesWei.map(({ sponsorshipId, differenceWei }) => ({ + adjustments.map(({ sponsorshipId, differenceWei }) => ({ type: differenceWei > 0n ? 'stake' : 'unstake', sponsorshipId, amount: differenceWei > 0n ? differenceWei : -differenceWei From 6d583bbe1da6a6dd89be451d8c973cce03c85bd0 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 21/60] filter out small transactions --- .../plugins/autostaker/AutostakerPlugin.ts | 2 +- .../autostaker/payoutProportionalStrategy.ts | 21 +++- packages/node/src/plugins/autostaker/types.ts | 3 +- .../payoutProportionalStrategy.test.ts | 99 ++++++++++++++++--- 4 files changed, 107 insertions(+), 18 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 858510b348..2dcec4d830 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -76,7 +76,7 @@ export class AutostakerPlugin extends Plugin { stakes, unstakedWei }, - operatorConfig: {}, // TODO add maxSponsorshipCount + operatorConfig: { minTransactionWei: 1000n }, // TODO add maxSponsorshipCount, what is a good value for minTransactionWei stakeableSponsorships, environmentConfig: { minimumStakeWei: 5000000000000000000000n // TODO read from The Graph (network.minimumStakeWei) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 7275e48852..02631ec2c7 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,5 +1,6 @@ import { sum, WeiAmount } from '@streamr/utils' import partition from 'lodash/partition' +import minBy from 'lodash/minBy' import maxBy from 'lodash/maxBy' import pull from 'lodash/pull' import sortBy from 'lodash/sortBy' @@ -39,6 +40,8 @@ import { Action, AdjustStakesFn, SponsorshipConfig, SponsorshipID } from './type type TargetStake = [SponsorshipID, WeiAmount] +const abs = (n: bigint) => (n < 0n) ? -n : n + const getExpiredSponsorships = (stakes: Map, stakeableSponsorships: Map): SponsorshipID[] => { return [...stakes.keys()].filter((sponsorshipId) => !stakeableSponsorships.has(sponsorshipId)) } @@ -122,18 +125,28 @@ export const adjustStakes: AdjustStakesFn = ({ .map((sponsorshipId) => ({ sponsorshipId, differenceWei: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) })) .filter(({ differenceWei: difference }) => difference !== 0n) - // TODO: filter out too small (TODO: decide what "too small" means) stakings and unstakings because those just waste gas - // fix rounding errors by forcing the net staking to equal unstakedWei: adjust the largest staking - const netStakingWei = sum(adjustments.map(({ differenceWei: difference }) => difference)) + const netStakingWei = sum(adjustments.map((a) => a.differenceWei)) if (netStakingWei !== operatorState.unstakedWei && stakeableSponsorships.size > 0 && adjustments.length > 0) { - const largestDifference = maxBy(adjustments, (a) => Number(d.differenceWei))! + const largestDifference = maxBy(adjustments, (a) => Number(a.differenceWei))! largestDifference.differenceWei += operatorState.unstakedWei - netStakingWei if (largestDifference.differenceWei === 0n) { pull(adjustments, largestDifference) } } + const tooSmallAdjustments = adjustments.filter((a) => abs(a.differenceWei) < operatorConfig.minTransactionWei) + if (tooSmallAdjustments.length > 0) { + pull(adjustments, ...tooSmallAdjustments) + let netChange = sum(tooSmallAdjustments.map((a) => a.differenceWei)) + while (netChange < 0) { + // there are more stakings than unstakings: remove smallest of the stakings + const smallestStaking = minBy(adjustments.filter((a) => a.differenceWei > 0), (a) => Number(a.differenceWei))! + pull(adjustments, smallestStaking) + netChange += smallestStaking.differenceWei + } + } + return sortBy( adjustments.map(({ sponsorshipId, differenceWei }) => ({ type: differenceWei > 0n ? 'stake' : 'unstake', diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index f76ecfc081..57e769faca 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -35,6 +35,7 @@ export interface OperatorState { export interface OperatorConfig { maxSponsorshipCount?: number + minTransactionWei: WeiAmount } /** @@ -42,6 +43,6 @@ export interface OperatorConfig { * @see https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L226 */ export interface EnvironmentConfig { - minimumStakeWei: WeiAmount + minimumStakeWei: WeiAmount // TODO rename to minStakeWei? } diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 84c2b56751..881ea0f6cc 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -5,7 +5,7 @@ describe('payoutProportionalStrategy', () => { it('stake all', () => { expect(adjustStakes({ operatorState: { unstakedWei: 11000n, stakes: new Map() }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 2n }], ['b', { totalPayoutWeiPerSec: 4n }], @@ -21,7 +21,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map(), environmentConfig: { minimumStakeWei: 1234n }, })).toEqual([ @@ -32,7 +32,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 600n, stakes: new Map() }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ['b', { totalPayoutWeiPerSec: 20n }], @@ -49,7 +49,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 500n, stakes: new Map() }, - operatorConfig: { maxSponsorshipCount: 2 }, + operatorConfig: { maxSponsorshipCount: 2, minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], // not included ['b', { totalPayoutWeiPerSec: 20n }], // included @@ -65,7 +65,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to minimumStakeWei and available tokens', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 500n, stakes: new Map() }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], // not included ['b', { totalPayoutWeiPerSec: 20n }], // not included @@ -80,7 +80,7 @@ describe('payoutProportionalStrategy', () => { it('doesn\'t allocate tokens if less available than minimum stake', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 100n, stakes: new Map() }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([['a', { totalPayoutWeiPerSec: 10n }]]), environmentConfig: { minimumStakeWei: 300n }, })).toEqual([]) @@ -93,7 +93,7 @@ describe('payoutProportionalStrategy', () => { [ 'a', 30n ], [ 'b', 70n ], ]) }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 40n }], // add stake here ['b', { totalPayoutWeiPerSec: 30n }], // unstake from here @@ -113,7 +113,7 @@ describe('payoutProportionalStrategy', () => { // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships expect(adjustStakes({ operatorState: { unstakedWei: 0n, stakes: new Map([[ 'b', 100n ]]) }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ]), @@ -131,7 +131,7 @@ describe('payoutProportionalStrategy', () => { [ 'b', 100n ], [ 'c', 100n ], ]) }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ['b', { totalPayoutWeiPerSec: 10n }], @@ -147,7 +147,7 @@ describe('payoutProportionalStrategy', () => { it('handles rounding errors', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map() }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n}, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 100n }], ['b', { totalPayoutWeiPerSec: 100n }], @@ -168,7 +168,7 @@ describe('payoutProportionalStrategy', () => { ['b', 166n ], ['c', 668n ], ]) }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 100n }], ['b', { totalPayoutWeiPerSec: 100n }], @@ -181,7 +181,7 @@ describe('payoutProportionalStrategy', () => { it('uses Infinity as default maxSponsorshipCount', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map() }, - operatorConfig: { }, + operatorConfig: { minTransactionWei: 0n }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ['b', { totalPayoutWeiPerSec: 20n }], @@ -191,4 +191,79 @@ describe('payoutProportionalStrategy', () => { environmentConfig: { minimumStakeWei: 0n }, })).toHaveLength(4) }) + + describe('exclude small transactions', () => { + it('exclude small stakings', () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorConfig: { minTransactionWei: 20n }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 10n }], + ['b', { totalPayoutWeiPerSec: 20n }], + ['c', { totalPayoutWeiPerSec: 1000n }] + ]), + environmentConfig: { minimumStakeWei: 0n }, + })).toIncludeSameMembers([ + { type: 'stake', sponsorshipId: 'c', amount: 972n } + ]) + }) + + it('one small transaction is balanced by removing one staking', () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 820n, stakes: new Map([ + ['a', 180n] + ]) }, + operatorConfig: { minTransactionWei: 20n }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 100n }], + ['b', { totalPayoutWeiPerSec: 100n }], + ['c', { totalPayoutWeiPerSec: 400n }], + ]), + environmentConfig: { minimumStakeWei: 0n } + })).toIncludeSameMembers([ + { type: 'stake', sponsorshipId: 'c', amount: 668n } + ]) + }) + + it('multiple small transactions are balanced with by removing multiple stakings', () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 740n, stakes: new Map([ + ['a', 180n], + ['b', 200n], + ['c', 295n] + ]) }, + operatorConfig: { minTransactionWei: 50n }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 100n }], + ['b', { totalPayoutWeiPerSec: 100n }], + ['c', { totalPayoutWeiPerSec: 210n }], + ['d', { totalPayoutWeiPerSec: 220n }], + ['e', { totalPayoutWeiPerSec: 230n }] + ]), + environmentConfig: { minimumStakeWei: 0n } + })).toIncludeSameMembers([ + { type: 'stake', sponsorshipId: 'e', amount: 381n } + ]) + }) + + it('multiple small transactions are balanced with by removing all stakings', () => { + expect(adjustStakes({ + operatorState: { unstakedWei: 359n, stakes: new Map([ + ['a', 180n], + ['b', 200n], + ['c', 295n], + ['e', 381n] + ]) }, + operatorConfig: { minTransactionWei: 50n }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 100n }], + ['b', { totalPayoutWeiPerSec: 100n }], + ['c', { totalPayoutWeiPerSec: 210n }], + ['d', { totalPayoutWeiPerSec: 220n }], + ['e', { totalPayoutWeiPerSec: 230n }] + ]), + environmentConfig: { minimumStakeWei: 0n } + })).toEqual([]) + }) + }) }) From 263605f19f44c53e736ae49ddf418eb31e17072a Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 22/60] rm comment --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 2dcec4d830..aafa787310 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -95,7 +95,6 @@ export class AutostakerPlugin extends Plugin { // eslint-disable-next-line class-methods-use-this private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { - // TODO is there a better way to get the client? Maybe we should add StreamrClient#getTheGraphClient() // TODO what are good where conditions for the sponsorships query so that we get all stakeable sponsorships // but no non-stakables (e.g. expired) const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { From e3e05eb12d0811bfe6fb4c1140a8a313dca20be6 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 23/60] scheduleAtApproximateInterval() --- packages/utils/src/exports.ts | 1 + .../src/scheduleAtApproximateInterval.ts | 32 ++++++++++++ .../scheduleAtApproximateInterval.test.ts | 51 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 packages/utils/src/scheduleAtApproximateInterval.ts create mode 100644 packages/utils/test/scheduleAtApproximateInterval.test.ts diff --git a/packages/utils/src/exports.ts b/packages/utils/src/exports.ts index d449928e4d..42c68a7d65 100644 --- a/packages/utils/src/exports.ts +++ b/packages/utils/src/exports.ts @@ -21,6 +21,7 @@ export { Multimap } from './Multimap' export { randomString } from './randomString' export { scheduleAtFixedRate } from './scheduleAtFixedRate' export { scheduleAtInterval } from './scheduleAtInterval' +export { scheduleAtApproximateInterval } from './scheduleAtApproximateInterval' export { toEthereumAddressOrENSName } from './toEthereumAddressOrENSName' export type { Events, BrandedString } from './types' export { wait } from './wait' diff --git a/packages/utils/src/scheduleAtApproximateInterval.ts b/packages/utils/src/scheduleAtApproximateInterval.ts new file mode 100644 index 0000000000..baad921f9d --- /dev/null +++ b/packages/utils/src/scheduleAtApproximateInterval.ts @@ -0,0 +1,32 @@ +import { scheduleAtInterval } from './scheduleAtInterval' +import { wait } from './wait' + +/* + * @param {number} approximateIntervalInMs - approximate time (in milliseconds) to wait after a task is completed + * The driftMultiplier defines how much the wait time can vary: e.g. if the interval is 60 minutes and the drift is 0.1, + * the delay between invocations will range from 54 to 66 minutes. + */ +export const scheduleAtApproximateInterval = async ( + task: () => Promise, + approximateIntervalInMs: number, + driftMultiplier: number, + executeAtStart: boolean, + abortSignal: AbortSignal +): Promise => { + if (abortSignal?.aborted) { + return + } + if (executeAtStart) { + await task() + } + return scheduleAtInterval(async () => { + try { + await wait(Math.round(Math.random() * approximateIntervalInMs * 2 * driftMultiplier), abortSignal) + } catch { + // the abort signal timeouted, ignore + } + if (!abortSignal.aborted) { + await task() + } + }, approximateIntervalInMs * (1 - driftMultiplier), false, abortSignal) +} diff --git a/packages/utils/test/scheduleAtApproximateInterval.test.ts b/packages/utils/test/scheduleAtApproximateInterval.test.ts new file mode 100644 index 0000000000..34bb9c7d7c --- /dev/null +++ b/packages/utils/test/scheduleAtApproximateInterval.test.ts @@ -0,0 +1,51 @@ +import { scheduleAtApproximateInterval } from '../src/scheduleAtApproximateInterval' +import { wait } from '../src/wait' + +const INTERVAL = 50 +const JITTER = INTERVAL * 2 +const DRIFT_MULTIPLIER = 0.1 +const AT_LEAST_FIVE_REPEATS_TIME = INTERVAL * 5 + JITTER + +describe('scheduleAtInterval', () => { + let task: jest.Mock, []> + let abortController: AbortController + + beforeEach(() => { + task = jest.fn() + abortController = new AbortController() + }) + + afterEach(() => { + abortController.abort() + }) + + it('execute at start enabled', async () => { + await scheduleAtApproximateInterval(task, INTERVAL, DRIFT_MULTIPLIER, true, abortController.signal) + expect(task).toHaveBeenCalledTimes(1) + }) + + it('execute at start disabled', async () => { + await scheduleAtApproximateInterval(task, INTERVAL, DRIFT_MULTIPLIER, false, abortController.signal) + expect(task).toHaveBeenCalledTimes(0) + }) + + it('repeats every `interval`', async () => { + await scheduleAtApproximateInterval(task, INTERVAL, DRIFT_MULTIPLIER, false, abortController.signal) + await wait(AT_LEAST_FIVE_REPEATS_TIME * (1 + DRIFT_MULTIPLIER)) + expect(task.mock.calls.length).toBeGreaterThanOrEqual(5) + }) + + it('does not take into account the time for the promise to settle', async () => { + task.mockImplementation(() => wait(INTERVAL)) + await scheduleAtApproximateInterval(task, INTERVAL, DRIFT_MULTIPLIER, false, abortController.signal) + await wait(AT_LEAST_FIVE_REPEATS_TIME * (1 + DRIFT_MULTIPLIER)) + expect(task.mock.calls.length).toBeLessThan(5) + }) + + it('task never invoked if initially aborted', async () => { + abortController.abort() + await scheduleAtApproximateInterval(task, INTERVAL, DRIFT_MULTIPLIER, true, abortController.signal) + await wait(AT_LEAST_FIVE_REPEATS_TIME * (1 + DRIFT_MULTIPLIER)) + expect(task).not.toHaveBeenCalled() + }) +}) From cc531fe9c7a55f9743662c5cc7df9c9ddcbff884 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 24/60] use scheduleAtApproximateInterval() --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index aafa787310..071e9d1e0c 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -1,5 +1,5 @@ import { _operatorContractUtils, SignerWithProvider, StreamrClient } from '@streamr/sdk' -import { collect, Logger, scheduleAtInterval, WeiAmount } from '@streamr/utils' +import { collect, Logger, scheduleAtApproximateInterval, scheduleAtInterval, wait, WeiAmount } from '@streamr/utils' import { Schema } from 'ajv' import { formatEther } from 'ethers' import { Plugin } from '../../Plugin' @@ -49,13 +49,13 @@ export class AutostakerPlugin extends Plugin { async start(streamrClient: StreamrClient): Promise { logger.info('Start autostaker plugin') - scheduleAtInterval(async () => { + scheduleAtApproximateInterval(async () => { try { await this.runActions(streamrClient) } catch (err) { logger.warn('Error while running autostaker actions', { err }) } - }, this.pluginConfig.runIntervalInMs, false, this.abortController.signal) + }, this.pluginConfig.runIntervalInMs, 0.1, false, this.abortController.signal) } private async runActions(streamrClient: StreamrClient): Promise { From 81dda137a13bfb886896a21b649cacecd559d715 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 25/60] select sponsorships based on operatorContractAddress if totalPayoutWeiPerSec values are same --- .../plugins/autostaker/AutostakerPlugin.ts | 5 +- .../autostaker/payoutProportionalStrategy.ts | 31 +++++++---- packages/node/src/plugins/autostaker/types.ts | 1 + .../payoutProportionalStrategy.test.ts | 53 +++++++++++++------ 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 071e9d1e0c..3da86bdff6 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -76,7 +76,10 @@ export class AutostakerPlugin extends Plugin { stakes, unstakedWei }, - operatorConfig: { minTransactionWei: 1000n }, // TODO add maxSponsorshipCount, what is a good value for minTransactionWei + operatorConfig: { // TODO add maxSponsorshipCount + minTransactionWei: 1000n, // TODO get from the plugin config, what would be a good default value? + operatorContractAddress: this.pluginConfig.operatorContractAddress + }, stakeableSponsorships, environmentConfig: { minimumStakeWei: 5000000000000000000000n // TODO read from The Graph (network.minimumStakeWei) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 02631ec2c7..027f1aca92 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,7 +1,8 @@ import { sum, WeiAmount } from '@streamr/utils' -import partition from 'lodash/partition' -import minBy from 'lodash/minBy' +import crypto from 'crypto' import maxBy from 'lodash/maxBy' +import minBy from 'lodash/minBy' +import partition from 'lodash/partition' import pull from 'lodash/pull' import sortBy from 'lodash/sortBy' import { Action, AdjustStakesFn, SponsorshipConfig, SponsorshipID } from './types' @@ -53,7 +54,8 @@ const getSelectedSponsorships = ( stakeableSponsorships: Map, totalStakeableWei: WeiAmount, minimumStakeWei: WeiAmount, - maxSponsorshipCount: number | undefined + maxSponsorshipCount: number | undefined, + operatorContractAddress: string ): SponsorshipID[] => { const count = Math.min( stakeableSponsorships.size, @@ -66,9 +68,17 @@ const getSelectedSponsorships = ( ] = partition([...stakeableSponsorships.keys()], (id) => stakes.has(id)) return [ ...keptSponsorships, - // TODO: add secondary sorting of potentialSponsorships based on operator ID + sponsorship ID - // idea is: in case of a tie, operators should stake to different sponsorships - ...sortBy(potentialSponsorships, (id) => -Number(stakeableSponsorships.get(id)!.totalPayoutWeiPerSec)) + ...sortBy(potentialSponsorships, + (id) => -Number(stakeableSponsorships.get(id)!.totalPayoutWeiPerSec), + (id) => { + // If totalPayoutWeiPerSec is same for multiple sponsorships, different operators should + // choose different sponsorships. Using hash of some operator-specific ID + sponsorshipId + // to determine the order. Here we use operatorContractAddress, but it could also + // be e.g. the nodeId. + const buffer = crypto.createHash('md5').update(operatorContractAddress + id).digest() + return buffer.readInt32LE(0) + } + ) ].slice(0, count) } @@ -82,7 +92,8 @@ const getTargetStakes = ( stakeableSponsorships: Map, unstakedWei: WeiAmount, minimumStakeWei: WeiAmount, - maxSponsorshipCount: number | undefined + maxSponsorshipCount: number | undefined, + operatorContractAddress: string ): Map => { const totalStakeableWei = sum([...stakes.values()]) + unstakedWei const selectedSponsorships = getSelectedSponsorships( @@ -90,7 +101,8 @@ const getTargetStakes = ( stakeableSponsorships, totalStakeableWei, minimumStakeWei, - maxSponsorshipCount + maxSponsorshipCount, + operatorContractAddress ) const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei const payoutProportionalWei = totalStakeableWei - minimumStakesWei @@ -118,7 +130,8 @@ export const adjustStakes: AdjustStakesFn = ({ stakeableSponsorships, operatorState.unstakedWei, environmentConfig.minimumStakeWei, - operatorConfig.maxSponsorshipCount + operatorConfig.maxSponsorshipCount, + operatorConfig.operatorContractAddress ) const adjustments = [...targetStakes.keys()] diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 57e769faca..2eb654fe4f 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -36,6 +36,7 @@ export interface OperatorState { export interface OperatorConfig { maxSponsorshipCount?: number minTransactionWei: WeiAmount + operatorContractAddress: string } /** diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 881ea0f6cc..8f18494e05 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -5,7 +5,7 @@ describe('payoutProportionalStrategy', () => { it('stake all', () => { expect(adjustStakes({ operatorState: { unstakedWei: 11000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 2n }], ['b', { totalPayoutWeiPerSec: 4n }], @@ -21,7 +21,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), environmentConfig: { minimumStakeWei: 1234n }, })).toEqual([ @@ -32,7 +32,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 600n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ['b', { totalPayoutWeiPerSec: 20n }], @@ -49,7 +49,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 500n, stakes: new Map() }, - operatorConfig: { maxSponsorshipCount: 2, minTransactionWei: 0n }, + operatorConfig: { maxSponsorshipCount: 2, minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], // not included ['b', { totalPayoutWeiPerSec: 20n }], // included @@ -65,7 +65,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to minimumStakeWei and available tokens', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 500n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], // not included ['b', { totalPayoutWeiPerSec: 20n }], // not included @@ -80,7 +80,7 @@ describe('payoutProportionalStrategy', () => { it('doesn\'t allocate tokens if less available than minimum stake', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 100n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { totalPayoutWeiPerSec: 10n }]]), environmentConfig: { minimumStakeWei: 300n }, })).toEqual([]) @@ -93,7 +93,7 @@ describe('payoutProportionalStrategy', () => { [ 'a', 30n ], [ 'b', 70n ], ]) }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 40n }], // add stake here ['b', { totalPayoutWeiPerSec: 30n }], // unstake from here @@ -113,7 +113,7 @@ describe('payoutProportionalStrategy', () => { // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships expect(adjustStakes({ operatorState: { unstakedWei: 0n, stakes: new Map([[ 'b', 100n ]]) }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ]), @@ -131,7 +131,7 @@ describe('payoutProportionalStrategy', () => { [ 'b', 100n ], [ 'c', 100n ], ]) }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ['b', { totalPayoutWeiPerSec: 10n }], @@ -147,7 +147,7 @@ describe('payoutProportionalStrategy', () => { it('handles rounding errors', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n}, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 100n }], ['b', { totalPayoutWeiPerSec: 100n }], @@ -168,7 +168,7 @@ describe('payoutProportionalStrategy', () => { ['b', 166n ], ['c', 668n ], ]) }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 100n }], ['b', { totalPayoutWeiPerSec: 100n }], @@ -181,7 +181,7 @@ describe('payoutProportionalStrategy', () => { it('uses Infinity as default maxSponsorshipCount', async () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ['b', { totalPayoutWeiPerSec: 20n }], @@ -192,11 +192,32 @@ describe('payoutProportionalStrategy', () => { })).toHaveLength(4) }) + it('operators may choose different sponsorships if totalPayoutWeiPerSec are same', () => { + const createArgs = (operatorContractAddress: string) => { + return { + operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorConfig: { minTransactionWei: 0n, operatorContractAddress }, + stakeableSponsorships: new Map([ + ['a', { totalPayoutWeiPerSec: 100n }], + ['b', { totalPayoutWeiPerSec: 100n }], + ]), + environmentConfig: { minimumStakeWei: 1000n }, + } + } + const stakesForOperator1 = adjustStakes(createArgs('0x1111')) + const stakesForOperator2 = adjustStakes(createArgs('0x2222')) + // may be different for different operators + expect(stakesForOperator1[0].sponsorshipId).not.toEqual(stakesForOperator2[0].sponsorshipId) + // but is deterministic for one operator + const stakesForOperator1_rerun = adjustStakes(createArgs('0x1111')) + expect(stakesForOperator1[0].sponsorshipId).toEqual(stakesForOperator1_rerun[0].sponsorshipId) + }) + describe('exclude small transactions', () => { it('exclude small stakings', () => { expect(adjustStakes({ operatorState: { unstakedWei: 1000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 20n }, + operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 10n }], ['b', { totalPayoutWeiPerSec: 20n }], @@ -213,7 +234,7 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 820n, stakes: new Map([ ['a', 180n] ]) }, - operatorConfig: { minTransactionWei: 20n }, + operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 100n }], ['b', { totalPayoutWeiPerSec: 100n }], @@ -232,7 +253,7 @@ describe('payoutProportionalStrategy', () => { ['b', 200n], ['c', 295n] ]) }, - operatorConfig: { minTransactionWei: 50n }, + operatorConfig: { minTransactionWei: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 100n }], ['b', { totalPayoutWeiPerSec: 100n }], @@ -254,7 +275,7 @@ describe('payoutProportionalStrategy', () => { ['c', 295n], ['e', 381n] ]) }, - operatorConfig: { minTransactionWei: 50n }, + operatorConfig: { minTransactionWei: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { totalPayoutWeiPerSec: 100n }], ['b', { totalPayoutWeiPerSec: 100n }], From edcb598770d4f0a0dacd2d200f532eaf43e65589 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 26/60] smoke test --- packages/node/test/smoke/autostaker.test.ts | 191 ++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 packages/node/test/smoke/autostaker.test.ts diff --git a/packages/node/test/smoke/autostaker.test.ts b/packages/node/test/smoke/autostaker.test.ts new file mode 100644 index 0000000000..946b9b05aa --- /dev/null +++ b/packages/node/test/smoke/autostaker.test.ts @@ -0,0 +1,191 @@ +import StreamrClient, { _operatorContractUtils, SignerWithProvider } from '@streamr/sdk' +import { + createTestPrivateKey, + createTestWallet +} from '@streamr/test-utils' +import { collect, Logger, StreamID, TheGraphClient, until, WeiAmount } from '@streamr/utils' +import { parseEther, Wallet } from 'ethers' +import { createBroker } from '../../src/broker' +import { SponsorshipID } from '../../src/plugins/autostaker/types' +import { createClient, createTestStream, deployTestOperatorContract, deployTestSponsorshipContract, formConfig } from '../utils' + +/* + * The test needs these dependencies: + * - clean dev-chain in Docker: + * streamr-docker-dev wipe && streamr-docker-dev start dev-chain-fast deploy-network-subgraphs-fastchain + * - DHT entry point: + * /bin/run-entry-point.sh + * + * Given: + * - one operator who has some delegated tokens + * - two sponsorships, both having relatively good payout + * + * When: + * - the operator starts to run a node with Autostaker plugin + * + * Then: + * - the operator stakes to both sponsorships + * + * When: + * - one of the sponsorships expire + * + * Then: + * - the operator unstakes from it + * + * When: + * - new sponsorship is created with relatively good payout + * + * Then: + * - the operator stakes to it + * + * When: + * - operator's delegated token balance is increased + * + * Then: + * - most/all of that balance is staked to sponsorships + */ + +const INITIAL_DELEGATED_AMOUNT = parseEther('500000') +const ADDITIONAL_DELEGATED_AMOUNT = parseEther('100000') +const SPONSORSHIP_1_EARNINGS_PER_SECOND = parseEther('100') +const SPONSORSHIP_1_SPONSOR_AMOUNT = parseEther('5000') +const SPONSORSHIP_2_EARNINGS_PER_SECOND = parseEther('200') +const SPONSORSHIP_2_SPONSOR_AMOUNT = parseEther('17000') +const SPONSORSHIP_3_EARNINGS_PER_SECOND = parseEther('300') +const SPONSORSHIP_3_SPONSOR_AMOUNT = parseEther('10000') +const RUN_INTERVAL = 10 * 1000 + +const logger = new Logger(module) + +const createStream = async (): Promise => { + const creator = createClient(await createTestPrivateKey({ gas: true })) + const stream = await createTestStream(creator, module) + await creator.destroy() + return stream.id +} + +const getStakes = async (operatorContractAddress: string, theGraphClient: TheGraphClient): Promise> => { + interface StakeQueryResultItem { + id: string + sponsorship: { + id: SponsorshipID + } + amountWei: WeiAmount + } + const queryResult = theGraphClient.queryEntities((lastId: string, pageSize: number) => { + return { + query: ` + { + stakes( + where: { + operator: "${operatorContractAddress.toLowerCase()}", + id_gt: "${lastId}" + }, + first: ${pageSize} + ) { + id + sponsorship { + id + } + amountWei + } + } + ` + } + }) + const stakes = await collect(queryResult) + return new Map(stakes.map((stake) => [stake.sponsorship.id, BigInt(stake.amountWei) ])) +} + +describe('autostaker', () => { + + let operatorContractAddress: string + let operator: Wallet & SignerWithProvider + let operatorNodePrivateKey: string + let sponsorshipId1: string + let sponsorshipId2: string + let sponsorer: SignerWithProvider + let theGraphClient: TheGraphClient + + beforeAll(async () => { + theGraphClient = new StreamrClient({ environment: 'dev2' }).getTheGraphClient() + operator = await createTestWallet({ gas: true, tokens: true }) + const operatorContract = await deployTestOperatorContract({ deployer: operator }) + operatorContractAddress = (await operatorContract.getAddress()).toLowerCase() + await _operatorContractUtils.delegate(operator, await operatorContract.getAddress(), INITIAL_DELEGATED_AMOUNT) + const operatorNodeWallet = await createTestWallet({ gas: true, tokens: true }) + operatorNodePrivateKey = operatorNodeWallet.privateKey + await (await operatorContract.grantRole(await operatorContract.CONTROLLER_ROLE(), operatorNodeWallet.address)).wait() + sponsorer = await createTestWallet({ gas: true, tokens: true }) + const sponsorship1 = await deployTestSponsorshipContract({ + earningsPerSecond: SPONSORSHIP_1_EARNINGS_PER_SECOND, + streamId: (await createStream()), + deployer: sponsorer + }) + await _operatorContractUtils.sponsor(sponsorer, await sponsorship1.getAddress(), SPONSORSHIP_1_SPONSOR_AMOUNT) + sponsorshipId1 = (await sponsorship1.getAddress()).toLowerCase() + const sponsorship2 = await deployTestSponsorshipContract({ + earningsPerSecond: SPONSORSHIP_2_EARNINGS_PER_SECOND, + streamId: (await createStream()), + deployer: sponsorer + }) + sponsorshipId2 = (await sponsorship2.getAddress()).toLowerCase() + await _operatorContractUtils.sponsor(sponsorer, await sponsorship2.getAddress(), SPONSORSHIP_2_SPONSOR_AMOUNT) + }) + + it('happy path', async () => { + + logger.info('Start', { + operatorContractAddress, + sponsorshipId1, + sponsorshipId2 + }) + const operatorNode = await createBroker(formConfig({ + privateKey: operatorNodePrivateKey, + extraPlugins: { + autostaker: { + operatorContractAddress, + runIntervalInMs: RUN_INTERVAL + } + } + })) + await operatorNode.start() + + await until(async () => { + const stakes = await getStakes(operatorContractAddress, theGraphClient) + return stakes.has(sponsorshipId1) && (stakes.has(sponsorshipId2)) + }, 60 * 1000, 1000) + logger.info('Both sponsorships have been staked') + + await until(async () => { + const stakes = await getStakes(operatorContractAddress, theGraphClient) + return !stakes.has(sponsorshipId1) + }, 5 * 60 * 1000, 1000) + logger.info('Expired sponsorship1 has been unstaked') + + const sponsorship3 = await deployTestSponsorshipContract({ + earningsPerSecond: SPONSORSHIP_3_EARNINGS_PER_SECOND, + streamId: (await createStream()), + deployer: sponsorer + }) + const sponsorshipId3 = (await sponsorship3.getAddress()).toLowerCase() + await _operatorContractUtils.sponsor(sponsorer, await sponsorship3.getAddress(), SPONSORSHIP_3_SPONSOR_AMOUNT) + + await until(async () => { + const stakes = await getStakes(operatorContractAddress, theGraphClient) + return stakes.has(sponsorshipId3) + }, 60 * 1000, 1000) + logger.info('New sponsorship3 have been staked') + + const amountBeforeAdditionalDelegation = (await getStakes(operatorContractAddress, theGraphClient)).get(sponsorshipId3)! + await _operatorContractUtils.delegate(operator, operatorContractAddress, ADDITIONAL_DELEGATED_AMOUNT) + await until(async () => { + const stakes = await getStakes(operatorContractAddress, theGraphClient) + const amount = stakes.get(sponsorshipId3)! + return amount > amountBeforeAdditionalDelegation + }, 60 * 1000, 1000) + logger.info('Stakes has been increased in some sponsorships') + + await operatorNode.stop() + }, 30 * 60 * 1000) +}) From b622b47b1d8abde39b57e330117b1201f59d52ed Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 27/60] eslint --- .../node/src/plugins/autostaker/AutostakerPlugin.ts | 4 ++-- .../plugins/autostaker/payoutProportionalStrategy.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 3da86bdff6..5aa87f7a5d 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -1,11 +1,11 @@ import { _operatorContractUtils, SignerWithProvider, StreamrClient } from '@streamr/sdk' -import { collect, Logger, scheduleAtApproximateInterval, scheduleAtInterval, wait, WeiAmount } from '@streamr/utils' +import { collect, Logger, scheduleAtApproximateInterval, WeiAmount } from '@streamr/utils' import { Schema } from 'ajv' import { formatEther } from 'ethers' import { Plugin } from '../../Plugin' import PLUGIN_CONFIG_SCHEMA from './config.schema.json' import { adjustStakes } from './payoutProportionalStrategy' -import { Action, SponsorshipID, SponsorshipConfig } from './types' +import { Action, SponsorshipConfig, SponsorshipID } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 027f1aca92..d7f563d673 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -43,7 +43,10 @@ type TargetStake = [SponsorshipID, WeiAmount] const abs = (n: bigint) => (n < 0n) ? -n : n -const getExpiredSponsorships = (stakes: Map, stakeableSponsorships: Map): SponsorshipID[] => { +const getExpiredSponsorships = ( + stakes: Map, + stakeableSponsorships: Map +): SponsorshipID[] => { return [...stakes.keys()].filter((sponsorshipId) => !stakeableSponsorships.has(sponsorshipId)) } /* @@ -135,7 +138,10 @@ export const adjustStakes: AdjustStakesFn = ({ ) const adjustments = [...targetStakes.keys()] - .map((sponsorshipId) => ({ sponsorshipId, differenceWei: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) })) + .map((sponsorshipId) => ({ + sponsorshipId, + differenceWei: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) + })) .filter(({ differenceWei: difference }) => difference !== 0n) // fix rounding errors by forcing the net staking to equal unstakedWei: adjust the largest staking From 9fe13e50ee309cde7e0eed866bf6b1579950e7ec Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 28/60] maxSponsorshipCount in plugin config --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 6 ++++-- packages/node/src/plugins/autostaker/config.schema.json | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 5aa87f7a5d..2e9c8888dc 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -10,6 +10,7 @@ import { Action, SponsorshipConfig, SponsorshipID } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string runIntervalInMs: number + maxSponsorshipCount?: number } interface SponsorshipQueryResultItem { @@ -76,9 +77,10 @@ export class AutostakerPlugin extends Plugin { stakes, unstakedWei }, - operatorConfig: { // TODO add maxSponsorshipCount + operatorConfig: { minTransactionWei: 1000n, // TODO get from the plugin config, what would be a good default value? - operatorContractAddress: this.pluginConfig.operatorContractAddress + operatorContractAddress: this.pluginConfig.operatorContractAddress, + maxSponsorshipCount: this.pluginConfig.maxSponsorshipCount }, stakeableSponsorships, environmentConfig: { diff --git a/packages/node/src/plugins/autostaker/config.schema.json b/packages/node/src/plugins/autostaker/config.schema.json index 5234b11836..283acc9886 100644 --- a/packages/node/src/plugins/autostaker/config.schema.json +++ b/packages/node/src/plugins/autostaker/config.schema.json @@ -18,6 +18,11 @@ "description": "The interval (in milliseconds) at which autostaking possibilities are analyzed and executed", "minimum": 0, "default": 30000 + }, + "maxSponsorshipCount": { + "type": "integer", + "description": "Maximum count of sponsorships which are staked at any given time", + "minimum": 1 } } } From cfce1544d573c23f01620fb2d728302a61abec40 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 29/60] minTransactionDataTokenAmount in plugin config --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 5 +++-- packages/node/src/plugins/autostaker/config.schema.json | 6 ++++++ packages/node/src/plugins/autostaker/types.ts | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 2e9c8888dc..3e1dbec3ce 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -1,7 +1,7 @@ import { _operatorContractUtils, SignerWithProvider, StreamrClient } from '@streamr/sdk' import { collect, Logger, scheduleAtApproximateInterval, WeiAmount } from '@streamr/utils' import { Schema } from 'ajv' -import { formatEther } from 'ethers' +import { formatEther, parseEther } from 'ethers' import { Plugin } from '../../Plugin' import PLUGIN_CONFIG_SCHEMA from './config.schema.json' import { adjustStakes } from './payoutProportionalStrategy' @@ -10,6 +10,7 @@ import { Action, SponsorshipConfig, SponsorshipID } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string runIntervalInMs: number + minTransactionDataTokenAmount: number maxSponsorshipCount?: number } @@ -78,8 +79,8 @@ export class AutostakerPlugin extends Plugin { unstakedWei }, operatorConfig: { - minTransactionWei: 1000n, // TODO get from the plugin config, what would be a good default value? operatorContractAddress: this.pluginConfig.operatorContractAddress, + minTransactionWei: parseEther(String(this.pluginConfig.minTransactionDataTokenAmount)), maxSponsorshipCount: this.pluginConfig.maxSponsorshipCount }, stakeableSponsorships, diff --git a/packages/node/src/plugins/autostaker/config.schema.json b/packages/node/src/plugins/autostaker/config.schema.json index 283acc9886..07c58d1d15 100644 --- a/packages/node/src/plugins/autostaker/config.schema.json +++ b/packages/node/src/plugins/autostaker/config.schema.json @@ -19,6 +19,12 @@ "minimum": 0, "default": 30000 }, + "minTransactionDataTokenAmount": { + "type": "integer", + "description": "Minimum data token amount for stake/unstake transaction", + "minimum": 0, + "default": 1000 + }, "maxSponsorshipCount": { "type": "integer", "description": "Maximum count of sponsorships which are staked at any given time", diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 2eb654fe4f..3300707c7b 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -34,9 +34,9 @@ export interface OperatorState { } export interface OperatorConfig { - maxSponsorshipCount?: number - minTransactionWei: WeiAmount operatorContractAddress: string + minTransactionWei: WeiAmount + maxSponsorshipCount?: number } /** From 4b9f5a2809ff2c04231adc0e5c122dbe0f0e2841 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 30/60] rename totalPayoutWeiPerSec -> payoutPerSec --- .../plugins/autostaker/AutostakerPlugin.ts | 2 +- .../autostaker/payoutProportionalStrategy.ts | 14 +-- packages/node/src/plugins/autostaker/types.ts | 5 +- .../payoutProportionalStrategy.test.ts | 98 +++++++++---------- 4 files changed, 58 insertions(+), 61 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 3e1dbec3ce..868a7c4b11 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -125,7 +125,7 @@ export class AutostakerPlugin extends Plugin { const sponsorships = await collect(queryResult) return new Map(sponsorships.map( (sponsorship) => [sponsorship.id, { - totalPayoutWeiPerSec: BigInt(sponsorship.totalPayoutWeiPerSec), + payoutPerSec: BigInt(sponsorship.totalPayoutWeiPerSec), }]) ) } diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index d7f563d673..2a035a68b7 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -11,9 +11,9 @@ import { Action, AdjustStakesFn, SponsorshipConfig, SponsorshipID } from './type * Allocate stake in proportion to the payout each sponsorship gives. * Detailed allocation formula: each sponsorship should get the stake M + P / T, * where M is the environment-mandated minimum stake, - * P is `totalPayoutWeiPerSec` of the considered sponsorship, and - * T is sum of `totalPayoutWeiPerSec` of all sponsorships this operator stakes to. - * `totalPayoutWeiPerSec` is the total payout per second to all staked operators. + * P is `payoutPerSec` of the considered sponsorship, and + * T is sum of `payoutPerSec` of all sponsorships this operator stakes to. + * `payoutPerSec` is the total payout per second to all staked operators. * * In order to prevent that operators' actions cause other operators to change their plans, * this strategy only takes into account the payout, not what others have staked. @@ -72,9 +72,9 @@ const getSelectedSponsorships = ( return [ ...keptSponsorships, ...sortBy(potentialSponsorships, - (id) => -Number(stakeableSponsorships.get(id)!.totalPayoutWeiPerSec), + (id) => -Number(stakeableSponsorships.get(id)!.payoutPerSec), (id) => { - // If totalPayoutWeiPerSec is same for multiple sponsorships, different operators should + // If payoutPerSec is same for multiple sponsorships, different operators should // choose different sponsorships. Using hash of some operator-specific ID + sponsorshipId // to determine the order. Here we use operatorContractAddress, but it could also // be e.g. the nodeId. @@ -109,10 +109,10 @@ const getTargetStakes = ( ) const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei const payoutProportionalWei = totalStakeableWei - minimumStakesWei - const payoutSumWeiPerSec = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.totalPayoutWeiPerSec)) + const payoutSumWeiPerSec = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.payoutPerSec)) const targetsForSelected: TargetStake[] = selectedSponsorships.map((id) => [ id, - minimumStakeWei + payoutProportionalWei * stakeableSponsorships.get(id)!.totalPayoutWeiPerSec / payoutSumWeiPerSec + minimumStakeWei + payoutProportionalWei * stakeableSponsorships.get(id)!.payoutPerSec / payoutSumWeiPerSec ]) const targetsForExpired: TargetStake[] = getExpiredSponsorships(stakes, stakeableSponsorships).map((id) => [ id, diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 3300707c7b..7a081fd58f 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -18,11 +18,8 @@ export type AdjustStakesFn = (opts: { environmentConfig: EnvironmentConfig }) => Action[] -/** - * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) - **/ export interface SponsorshipConfig { - totalPayoutWeiPerSec: WeiAmount + payoutPerSec: WeiAmount } /** diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 8f18494e05..152a77c12e 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -7,9 +7,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 11000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 2n }], - ['b', { totalPayoutWeiPerSec: 4n }], - ['c', { totalPayoutWeiPerSec: 6n }], + ['a', { payoutPerSec: 2n }], + ['b', { payoutPerSec: 4n }], + ['c', { payoutPerSec: 6n }], ]), environmentConfig: { minimumStakeWei: 5000n }, })).toIncludeSameMembers([ @@ -34,9 +34,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 600n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n }], - ['b', { totalPayoutWeiPerSec: 20n }], - ['c', { totalPayoutWeiPerSec: 30n }], + ['a', { payoutPerSec: 10n }], + ['b', { payoutPerSec: 20n }], + ['c', { payoutPerSec: 30n }], ]), environmentConfig: { minimumStakeWei: 0n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -51,9 +51,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 500n, stakes: new Map() }, operatorConfig: { maxSponsorshipCount: 2, minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n }], // not included - ['b', { totalPayoutWeiPerSec: 20n }], // included - ['c', { totalPayoutWeiPerSec: 30n }], // included + ['a', { payoutPerSec: 10n }], // not included + ['b', { payoutPerSec: 20n }], // included + ['c', { payoutPerSec: 30n }], // included ]), environmentConfig: { minimumStakeWei: 0n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -67,9 +67,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 500n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n }], // not included - ['b', { totalPayoutWeiPerSec: 20n }], // not included - ['c', { totalPayoutWeiPerSec: 30n }], // included + ['a', { payoutPerSec: 10n }], // not included + ['b', { payoutPerSec: 20n }], // not included + ['c', { payoutPerSec: 30n }], // included ]), environmentConfig: { minimumStakeWei: 300n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -81,7 +81,7 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ operatorState: { unstakedWei: 100n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, - stakeableSponsorships: new Map([['a', { totalPayoutWeiPerSec: 10n }]]), + stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), environmentConfig: { minimumStakeWei: 300n }, })).toEqual([]) }) @@ -95,10 +95,10 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 40n }], // add stake here - ['b', { totalPayoutWeiPerSec: 30n }], // unstake from here - ['c', { totalPayoutWeiPerSec: 20n }], // stake here - ['d', { totalPayoutWeiPerSec: 10n }], // stake here + ['a', { payoutPerSec: 40n }], // add stake here + ['b', { payoutPerSec: 30n }], // unstake from here + ['c', { payoutPerSec: 20n }], // stake here + ['d', { payoutPerSec: 10n }], // stake here ]), environmentConfig: { minimumStakeWei: 0n }, }).map((a) => a.type)).toEqual([ @@ -115,7 +115,7 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 0n, stakes: new Map([[ 'b', 100n ]]) }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n }], + ['a', { payoutPerSec: 10n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toIncludeSameMembers([ @@ -133,8 +133,8 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n }], - ['b', { totalPayoutWeiPerSec: 10n }], + ['a', { payoutPerSec: 10n }], + ['b', { payoutPerSec: 10n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toIncludeSameMembers([ @@ -149,9 +149,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n }], - ['b', { totalPayoutWeiPerSec: 100n }], - ['c', { totalPayoutWeiPerSec: 400n }], + ['a', { payoutPerSec: 100n }], + ['b', { payoutPerSec: 100n }], + ['c', { payoutPerSec: 400n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toIncludeSameMembers([ @@ -170,9 +170,9 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n }], - ['b', { totalPayoutWeiPerSec: 100n }], - ['c', { totalPayoutWeiPerSec: 400n }], + ['a', { payoutPerSec: 100n }], + ['b', { payoutPerSec: 100n }], + ['c', { payoutPerSec: 400n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toEqual([]) @@ -183,23 +183,23 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n }], - ['b', { totalPayoutWeiPerSec: 20n }], - ['c', { totalPayoutWeiPerSec: 30n }], - ['d', { totalPayoutWeiPerSec: 40n }], + ['a', { payoutPerSec: 10n }], + ['b', { payoutPerSec: 20n }], + ['c', { payoutPerSec: 30n }], + ['d', { payoutPerSec: 40n }], ]), environmentConfig: { minimumStakeWei: 0n }, })).toHaveLength(4) }) - it('operators may choose different sponsorships if totalPayoutWeiPerSec are same', () => { + it('operators may choose different sponsorships if payoutPerSec are same', () => { const createArgs = (operatorContractAddress: string) => { return { operatorState: { unstakedWei: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n }], - ['b', { totalPayoutWeiPerSec: 100n }], + ['a', { payoutPerSec: 100n }], + ['b', { payoutPerSec: 100n }], ]), environmentConfig: { minimumStakeWei: 1000n }, } @@ -219,9 +219,9 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedWei: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 10n }], - ['b', { totalPayoutWeiPerSec: 20n }], - ['c', { totalPayoutWeiPerSec: 1000n }] + ['a', { payoutPerSec: 10n }], + ['b', { payoutPerSec: 20n }], + ['c', { payoutPerSec: 1000n }] ]), environmentConfig: { minimumStakeWei: 0n }, })).toIncludeSameMembers([ @@ -236,9 +236,9 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n }], - ['b', { totalPayoutWeiPerSec: 100n }], - ['c', { totalPayoutWeiPerSec: 400n }], + ['a', { payoutPerSec: 100n }], + ['b', { payoutPerSec: 100n }], + ['c', { payoutPerSec: 400n }], ]), environmentConfig: { minimumStakeWei: 0n } })).toIncludeSameMembers([ @@ -255,11 +255,11 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { minTransactionWei: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n }], - ['b', { totalPayoutWeiPerSec: 100n }], - ['c', { totalPayoutWeiPerSec: 210n }], - ['d', { totalPayoutWeiPerSec: 220n }], - ['e', { totalPayoutWeiPerSec: 230n }] + ['a', { payoutPerSec: 100n }], + ['b', { payoutPerSec: 100n }], + ['c', { payoutPerSec: 210n }], + ['d', { payoutPerSec: 220n }], + ['e', { payoutPerSec: 230n }] ]), environmentConfig: { minimumStakeWei: 0n } })).toIncludeSameMembers([ @@ -277,11 +277,11 @@ describe('payoutProportionalStrategy', () => { ]) }, operatorConfig: { minTransactionWei: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ - ['a', { totalPayoutWeiPerSec: 100n }], - ['b', { totalPayoutWeiPerSec: 100n }], - ['c', { totalPayoutWeiPerSec: 210n }], - ['d', { totalPayoutWeiPerSec: 220n }], - ['e', { totalPayoutWeiPerSec: 230n }] + ['a', { payoutPerSec: 100n }], + ['b', { payoutPerSec: 100n }], + ['c', { payoutPerSec: 210n }], + ['d', { payoutPerSec: 220n }], + ['e', { payoutPerSec: 230n }] ]), environmentConfig: { minimumStakeWei: 0n } })).toEqual([]) From 4c3c0d74838dff085ccedff04d3d7533315e59f1 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 31/60] rename unstakedWei -> unstakedAmount --- .../plugins/autostaker/AutostakerPlugin.ts | 10 +++--- .../autostaker/payoutProportionalStrategy.ts | 12 +++---- packages/node/src/plugins/autostaker/types.ts | 5 +-- .../payoutProportionalStrategy.test.ts | 34 +++++++++---------- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 868a7c4b11..53bb08ba59 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -65,18 +65,18 @@ export class AutostakerPlugin extends Plugin { const provider = (await streamrClient.getSigner()).provider const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) .connect(provider) - const stakedWei = await operatorContract.totalStakedIntoSponsorshipsWei() - const unstakedWei = (await operatorContract.valueWithoutEarnings()) - stakedWei - logger.info(`Balance: unstaked=${formatEther(unstakedWei)}, staked=${formatEther(stakedWei)}`) + const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() + const unstakedAmount = (await operatorContract.valueWithoutEarnings()) - stakedAmount + logger.info(`Balance: unstaked=${formatEther(unstakedAmount)}, staked=${formatEther(stakedAmount)}`) const stakeableSponsorships = await this.getStakeableSponsorships(streamrClient) logger.info(`Stakeable sponsorships: ${[...stakeableSponsorships.keys()].join(',')}`) const stakes = await this.getStakes(streamrClient) - const stakeDescription = [...stakes.entries()].map(([sponsorshipId, amountWei]) => `${sponsorshipId}=${formatEther(amountWei)}`).join(', ') + const stakeDescription = [...stakes.entries()].map(([sponsorshipId, amount]) => `${sponsorshipId}=${formatEther(amount)}`).join(', ') logger.info(`Stakes before adjustments: ${stakeDescription}`) const actions = adjustStakes({ operatorState: { stakes, - unstakedWei + unstakedAmount }, operatorConfig: { operatorContractAddress: this.pluginConfig.operatorContractAddress, diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 2a035a68b7..8351e60c2f 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -93,12 +93,12 @@ const getSelectedSponsorships = ( const getTargetStakes = ( stakes: Map, stakeableSponsorships: Map, - unstakedWei: WeiAmount, + unstakedAmount: WeiAmount, minimumStakeWei: WeiAmount, maxSponsorshipCount: number | undefined, operatorContractAddress: string ): Map => { - const totalStakeableWei = sum([...stakes.values()]) + unstakedWei + const totalStakeableWei = sum([...stakes.values()]) + unstakedAmount const selectedSponsorships = getSelectedSponsorships( stakes, stakeableSponsorships, @@ -131,7 +131,7 @@ export const adjustStakes: AdjustStakesFn = ({ const targetStakes = getTargetStakes( operatorState.stakes, stakeableSponsorships, - operatorState.unstakedWei, + operatorState.unstakedAmount, environmentConfig.minimumStakeWei, operatorConfig.maxSponsorshipCount, operatorConfig.operatorContractAddress @@ -144,11 +144,11 @@ export const adjustStakes: AdjustStakesFn = ({ })) .filter(({ differenceWei: difference }) => difference !== 0n) - // fix rounding errors by forcing the net staking to equal unstakedWei: adjust the largest staking + // fix rounding errors by forcing the net staking to equal unstakedAmount: adjust the largest staking const netStakingWei = sum(adjustments.map((a) => a.differenceWei)) - if (netStakingWei !== operatorState.unstakedWei && stakeableSponsorships.size > 0 && adjustments.length > 0) { + if (netStakingWei !== operatorState.unstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { const largestDifference = maxBy(adjustments, (a) => Number(a.differenceWei))! - largestDifference.differenceWei += operatorState.unstakedWei - netStakingWei + largestDifference.differenceWei += operatorState.unstakedAmount - netStakingWei if (largestDifference.differenceWei === 0n) { pull(adjustments, largestDifference) } diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 7a081fd58f..4b0abcdcc0 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -22,12 +22,9 @@ export interface SponsorshipConfig { payoutPerSec: WeiAmount } -/** - * Namings here reflect [thegraph schema](https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L435) - **/ export interface OperatorState { stakes: Map - unstakedWei: WeiAmount + unstakedAmount: WeiAmount } export interface OperatorConfig { diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 152a77c12e..dac6f21b2e 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -4,7 +4,7 @@ describe('payoutProportionalStrategy', () => { it('stake all', () => { expect(adjustStakes({ - operatorState: { unstakedWei: 11000n, stakes: new Map() }, + operatorState: { unstakedAmount: 11000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 2n }], @@ -20,7 +20,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, + operatorState: { unstakedAmount: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), environmentConfig: { minimumStakeWei: 1234n }, @@ -31,7 +31,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 600n, stakes: new Map() }, + operatorState: { unstakedAmount: 600n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -48,7 +48,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 500n, stakes: new Map() }, + operatorState: { unstakedAmount: 500n, stakes: new Map() }, operatorConfig: { maxSponsorshipCount: 2, minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -64,7 +64,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to minimumStakeWei and available tokens', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 500n, stakes: new Map() }, + operatorState: { unstakedAmount: 500n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -79,7 +79,7 @@ describe('payoutProportionalStrategy', () => { it('doesn\'t allocate tokens if less available than minimum stake', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 100n, stakes: new Map() }, + operatorState: { unstakedAmount: 100n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), environmentConfig: { minimumStakeWei: 300n }, @@ -89,7 +89,7 @@ describe('payoutProportionalStrategy', () => { // unstakes must happen first because otherwise there isn't enough tokens for staking it('sends out unstakes before stakes', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, stakes: new Map([ [ 'a', 30n ], [ 'b', 70n ], ]) }, @@ -112,7 +112,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes from expired sponsorships', async () => { // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships expect(adjustStakes({ - operatorState: { unstakedWei: 0n, stakes: new Map([[ 'b', 100n ]]) }, + operatorState: { unstakedAmount: 0n, stakes: new Map([[ 'b', 100n ]]) }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -126,7 +126,7 @@ describe('payoutProportionalStrategy', () => { it('restakes expired sponsorship stakes into other sponsorships', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, stakes: new Map([ [ 'a', 100n ], [ 'b', 100n ], [ 'c', 100n ], @@ -146,7 +146,7 @@ describe('payoutProportionalStrategy', () => { it('handles rounding errors', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -163,7 +163,7 @@ describe('payoutProportionalStrategy', () => { it('rounding error no-op case', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, stakes: new Map([ ['a', 166n ], ['b', 166n ], ['c', 668n ], @@ -180,7 +180,7 @@ describe('payoutProportionalStrategy', () => { it('uses Infinity as default maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -195,7 +195,7 @@ describe('payoutProportionalStrategy', () => { it('operators may choose different sponsorships if payoutPerSec are same', () => { const createArgs = (operatorContractAddress: string) => { return { - operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 0n, operatorContractAddress }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -216,7 +216,7 @@ describe('payoutProportionalStrategy', () => { describe('exclude small transactions', () => { it('exclude small stakings', () => { expect(adjustStakes({ - operatorState: { unstakedWei: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, stakes: new Map() }, operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -231,7 +231,7 @@ describe('payoutProportionalStrategy', () => { it('one small transaction is balanced by removing one staking', () => { expect(adjustStakes({ - operatorState: { unstakedWei: 820n, stakes: new Map([ + operatorState: { unstakedAmount: 820n, stakes: new Map([ ['a', 180n] ]) }, operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, @@ -248,7 +248,7 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing multiple stakings', () => { expect(adjustStakes({ - operatorState: { unstakedWei: 740n, stakes: new Map([ + operatorState: { unstakedAmount: 740n, stakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n] @@ -269,7 +269,7 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing all stakings', () => { expect(adjustStakes({ - operatorState: { unstakedWei: 359n, stakes: new Map([ + operatorState: { unstakedAmount: 359n, stakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n], From e19898ffd36da058d48652c4335867fd3ab66a25 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 32/60] rename minTransactionWei -> minTransactionAmount --- .../plugins/autostaker/AutostakerPlugin.ts | 2 +- .../autostaker/payoutProportionalStrategy.ts | 2 +- packages/node/src/plugins/autostaker/types.ts | 2 +- .../payoutProportionalStrategy.test.ts | 34 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 53bb08ba59..5b3a4c280a 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -80,7 +80,7 @@ export class AutostakerPlugin extends Plugin { }, operatorConfig: { operatorContractAddress: this.pluginConfig.operatorContractAddress, - minTransactionWei: parseEther(String(this.pluginConfig.minTransactionDataTokenAmount)), + minTransactionAmount: parseEther(String(this.pluginConfig.minTransactionDataTokenAmount)), maxSponsorshipCount: this.pluginConfig.maxSponsorshipCount }, stakeableSponsorships, diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 8351e60c2f..1779ea911c 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -154,7 +154,7 @@ export const adjustStakes: AdjustStakesFn = ({ } } - const tooSmallAdjustments = adjustments.filter((a) => abs(a.differenceWei) < operatorConfig.minTransactionWei) + const tooSmallAdjustments = adjustments.filter((a) => abs(a.differenceWei) < operatorConfig.minTransactionAmount) if (tooSmallAdjustments.length > 0) { pull(adjustments, ...tooSmallAdjustments) let netChange = sum(tooSmallAdjustments.map((a) => a.differenceWei)) diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 4b0abcdcc0..9887c67224 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -29,7 +29,7 @@ export interface OperatorState { export interface OperatorConfig { operatorContractAddress: string - minTransactionWei: WeiAmount + minTransactionAmount: WeiAmount maxSponsorshipCount?: number } diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index dac6f21b2e..d92e187695 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -5,7 +5,7 @@ describe('payoutProportionalStrategy', () => { it('stake all', () => { expect(adjustStakes({ operatorState: { unstakedAmount: 11000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 2n }], ['b', { payoutPerSec: 4n }], @@ -21,7 +21,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), environmentConfig: { minimumStakeWei: 1234n }, })).toEqual([ @@ -32,7 +32,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 600n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 20n }], @@ -49,7 +49,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 500n, stakes: new Map() }, - operatorConfig: { maxSponsorshipCount: 2, minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { maxSponsorshipCount: 2, minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included ['b', { payoutPerSec: 20n }], // included @@ -65,7 +65,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to minimumStakeWei and available tokens', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 500n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included ['b', { payoutPerSec: 20n }], // not included @@ -80,7 +80,7 @@ describe('payoutProportionalStrategy', () => { it('doesn\'t allocate tokens if less available than minimum stake', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 100n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), environmentConfig: { minimumStakeWei: 300n }, })).toEqual([]) @@ -93,7 +93,7 @@ describe('payoutProportionalStrategy', () => { [ 'a', 30n ], [ 'b', 70n ], ]) }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 40n }], // add stake here ['b', { payoutPerSec: 30n }], // unstake from here @@ -113,7 +113,7 @@ describe('payoutProportionalStrategy', () => { // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships expect(adjustStakes({ operatorState: { unstakedAmount: 0n, stakes: new Map([[ 'b', 100n ]]) }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ]), @@ -131,7 +131,7 @@ describe('payoutProportionalStrategy', () => { [ 'b', 100n ], [ 'c', 100n ], ]) }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 10n }], @@ -147,7 +147,7 @@ describe('payoutProportionalStrategy', () => { it('handles rounding errors', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 1000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], @@ -168,7 +168,7 @@ describe('payoutProportionalStrategy', () => { ['b', 166n ], ['c', 668n ], ]) }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], @@ -181,7 +181,7 @@ describe('payoutProportionalStrategy', () => { it('uses Infinity as default maxSponsorshipCount', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 1000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 20n }], @@ -196,7 +196,7 @@ describe('payoutProportionalStrategy', () => { const createArgs = (operatorContractAddress: string) => { return { operatorState: { unstakedAmount: 1000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 0n, operatorContractAddress }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], @@ -217,7 +217,7 @@ describe('payoutProportionalStrategy', () => { it('exclude small stakings', () => { expect(adjustStakes({ operatorState: { unstakedAmount: 1000n, stakes: new Map() }, - operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 20n }], @@ -234,7 +234,7 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedAmount: 820n, stakes: new Map([ ['a', 180n] ]) }, - operatorConfig: { minTransactionWei: 20n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], @@ -253,7 +253,7 @@ describe('payoutProportionalStrategy', () => { ['b', 200n], ['c', 295n] ]) }, - operatorConfig: { minTransactionWei: 50n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], @@ -275,7 +275,7 @@ describe('payoutProportionalStrategy', () => { ['c', 295n], ['e', 381n] ]) }, - operatorConfig: { minTransactionWei: 50n, operatorContractAddress: '' }, + operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], From 82c6177981e091c880422e6f59037d63a4ad64ae Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 33/60] rename minimumStakeWei -> minStakePerSponsorship --- .../plugins/autostaker/AutostakerPlugin.ts | 2 +- .../autostaker/payoutProportionalStrategy.ts | 14 ++++---- packages/node/src/plugins/autostaker/types.ts | 6 +--- .../payoutProportionalStrategy.test.ts | 36 +++++++++---------- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 5b3a4c280a..33e3b231cd 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -85,7 +85,7 @@ export class AutostakerPlugin extends Plugin { }, stakeableSponsorships, environmentConfig: { - minimumStakeWei: 5000000000000000000000n // TODO read from The Graph (network.minimumStakeWei) + minStakePerSponsorship: 5000000000000000000000n // TODO read from The Graph (network.minimumStakeWei) } }) const signer = await streamrClient.getSigner() diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 1779ea911c..097840ceb6 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -56,14 +56,14 @@ const getSelectedSponsorships = ( stakes: Map, stakeableSponsorships: Map, totalStakeableWei: WeiAmount, - minimumStakeWei: WeiAmount, + minStakePerSponsorship: WeiAmount, maxSponsorshipCount: number | undefined, operatorContractAddress: string ): SponsorshipID[] => { const count = Math.min( stakeableSponsorships.size, maxSponsorshipCount ?? Infinity, - Math.floor(Number(totalStakeableWei) / Number(minimumStakeWei)) // as many as we can afford + Math.floor(Number(totalStakeableWei) / Number(minStakePerSponsorship)) // as many as we can afford ) const [ keptSponsorships, @@ -94,7 +94,7 @@ const getTargetStakes = ( stakes: Map, stakeableSponsorships: Map, unstakedAmount: WeiAmount, - minimumStakeWei: WeiAmount, + minStakePerSponsorship: WeiAmount, maxSponsorshipCount: number | undefined, operatorContractAddress: string ): Map => { @@ -103,16 +103,16 @@ const getTargetStakes = ( stakes, stakeableSponsorships, totalStakeableWei, - minimumStakeWei, + minStakePerSponsorship, maxSponsorshipCount, operatorContractAddress ) - const minimumStakesWei = BigInt(selectedSponsorships.length) * minimumStakeWei + const minimumStakesWei = BigInt(selectedSponsorships.length) * minStakePerSponsorship const payoutProportionalWei = totalStakeableWei - minimumStakesWei const payoutSumWeiPerSec = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.payoutPerSec)) const targetsForSelected: TargetStake[] = selectedSponsorships.map((id) => [ id, - minimumStakeWei + payoutProportionalWei * stakeableSponsorships.get(id)!.payoutPerSec / payoutSumWeiPerSec + minStakePerSponsorship + payoutProportionalWei * stakeableSponsorships.get(id)!.payoutPerSec / payoutSumWeiPerSec ]) const targetsForExpired: TargetStake[] = getExpiredSponsorships(stakes, stakeableSponsorships).map((id) => [ id, @@ -132,7 +132,7 @@ export const adjustStakes: AdjustStakesFn = ({ operatorState.stakes, stakeableSponsorships, operatorState.unstakedAmount, - environmentConfig.minimumStakeWei, + environmentConfig.minStakePerSponsorship, operatorConfig.maxSponsorshipCount, operatorConfig.operatorContractAddress ) diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 9887c67224..3eb3761618 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -33,11 +33,7 @@ export interface OperatorConfig { maxSponsorshipCount?: number } -/** - * Network-wide constants, also available in thegraph - * @see https://github.com/streamr-dev/network-contracts/blob/master/packages/network-subgraphs/schema.graphql#L226 - */ export interface EnvironmentConfig { - minimumStakeWei: WeiAmount // TODO rename to minStakeWei? + minStakePerSponsorship: WeiAmount } diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index d92e187695..d555472c1c 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -11,7 +11,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 4n }], ['c', { payoutPerSec: 6n }], ]), - environmentConfig: { minimumStakeWei: 5000n }, + environmentConfig: { minStakePerSponsorship: 5000n }, })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'b', amount: 5400n }, { type: 'stake', sponsorshipId: 'c', amount: 5600n } @@ -23,7 +23,7 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedAmount: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), - environmentConfig: { minimumStakeWei: 1234n }, + environmentConfig: { minStakePerSponsorship: 1234n }, })).toEqual([ { type: 'unstake', sponsorshipId: 'a', amount: 2000n }, ]) @@ -38,7 +38,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 20n }], ['c', { payoutPerSec: 30n }], ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ { type: 'stake', sponsorshipId: 'a', amount: 100n }, { type: 'stake', sponsorshipId: 'b', amount: 200n }, @@ -55,14 +55,14 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 20n }], // included ['c', { payoutPerSec: 30n }], // included ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ { type: 'stake', sponsorshipId: 'b', amount: 200n }, { type: 'stake', sponsorshipId: 'c', amount: 300n }, ]) }) - it('limits the targetSponsorshipCount to minimumStakeWei and available tokens', async () => { + it('limits the targetSponsorshipCount to minStakePerSponsorship and available tokens', async () => { expect(adjustStakes({ operatorState: { unstakedAmount: 500n, stakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, @@ -71,7 +71,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 20n }], // not included ['c', { payoutPerSec: 30n }], // included ]), - environmentConfig: { minimumStakeWei: 300n }, + environmentConfig: { minStakePerSponsorship: 300n }, }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ { type: 'stake', sponsorshipId: 'c', amount: 500n }, ]) @@ -82,7 +82,7 @@ describe('payoutProportionalStrategy', () => { operatorState: { unstakedAmount: 100n, stakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), - environmentConfig: { minimumStakeWei: 300n }, + environmentConfig: { minStakePerSponsorship: 300n }, })).toEqual([]) }) @@ -100,7 +100,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 20n }], // stake here ['d', { payoutPerSec: 10n }], // stake here ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, }).map((a) => a.type)).toEqual([ 'unstake', 'stake', @@ -117,7 +117,7 @@ describe('payoutProportionalStrategy', () => { stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'b', amount: 100n }, { type: 'stake', sponsorshipId: 'a', amount: 100n }, @@ -136,7 +136,7 @@ describe('payoutProportionalStrategy', () => { ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 10n }], ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'c', amount: 100n }, { type: 'stake', sponsorshipId: 'a', amount: 50n }, @@ -153,7 +153,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 100n }], ['c', { payoutPerSec: 400n }], ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'a', amount: 166n }, { type: 'stake', sponsorshipId: 'b', amount: 166n }, @@ -174,7 +174,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 100n }], ['c', { payoutPerSec: 400n }], ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, })).toEqual([]) }) @@ -188,7 +188,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 30n }], ['d', { payoutPerSec: 40n }], ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, })).toHaveLength(4) }) @@ -201,7 +201,7 @@ describe('payoutProportionalStrategy', () => { ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], ]), - environmentConfig: { minimumStakeWei: 1000n }, + environmentConfig: { minStakePerSponsorship: 1000n }, } } const stakesForOperator1 = adjustStakes(createArgs('0x1111')) @@ -223,7 +223,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 20n }], ['c', { payoutPerSec: 1000n }] ]), - environmentConfig: { minimumStakeWei: 0n }, + environmentConfig: { minStakePerSponsorship: 0n }, })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'c', amount: 972n } ]) @@ -240,7 +240,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 100n }], ['c', { payoutPerSec: 400n }], ]), - environmentConfig: { minimumStakeWei: 0n } + environmentConfig: { minStakePerSponsorship: 0n } })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'c', amount: 668n } ]) @@ -261,7 +261,7 @@ describe('payoutProportionalStrategy', () => { ['d', { payoutPerSec: 220n }], ['e', { payoutPerSec: 230n }] ]), - environmentConfig: { minimumStakeWei: 0n } + environmentConfig: { minStakePerSponsorship: 0n } })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'e', amount: 381n } ]) @@ -283,7 +283,7 @@ describe('payoutProportionalStrategy', () => { ['d', { payoutPerSec: 220n }], ['e', { payoutPerSec: 230n }] ]), - environmentConfig: { minimumStakeWei: 0n } + environmentConfig: { minStakePerSponsorship: 0n } })).toEqual([]) }) }) From 3b71b7ba2a7e819d37695d26459173c5398fc549 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 34/60] rename local variables --- .../autostaker/payoutProportionalStrategy.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 097840ceb6..52833a16f9 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -55,7 +55,7 @@ const getExpiredSponsorships = ( const getSelectedSponsorships = ( stakes: Map, stakeableSponsorships: Map, - totalStakeableWei: WeiAmount, + totalStakeableAmount: WeiAmount, minStakePerSponsorship: WeiAmount, maxSponsorshipCount: number | undefined, operatorContractAddress: string @@ -63,7 +63,7 @@ const getSelectedSponsorships = ( const count = Math.min( stakeableSponsorships.size, maxSponsorshipCount ?? Infinity, - Math.floor(Number(totalStakeableWei) / Number(minStakePerSponsorship)) // as many as we can afford + Math.floor(Number(totalStakeableAmount) / Number(minStakePerSponsorship)) // as many as we can afford ) const [ keptSponsorships, @@ -98,21 +98,21 @@ const getTargetStakes = ( maxSponsorshipCount: number | undefined, operatorContractAddress: string ): Map => { - const totalStakeableWei = sum([...stakes.values()]) + unstakedAmount + const totalStakeableAmount = sum([...stakes.values()]) + unstakedAmount const selectedSponsorships = getSelectedSponsorships( stakes, stakeableSponsorships, - totalStakeableWei, + totalStakeableAmount, minStakePerSponsorship, maxSponsorshipCount, operatorContractAddress ) - const minimumStakesWei = BigInt(selectedSponsorships.length) * minStakePerSponsorship - const payoutProportionalWei = totalStakeableWei - minimumStakesWei - const payoutSumWeiPerSec = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.payoutPerSec)) + const minStakePerSponsorshipSum = BigInt(selectedSponsorships.length) * minStakePerSponsorship + const payoutProportionalAmount = totalStakeableAmount - minStakePerSponsorshipSum + const payoutPerSecSum = sum(selectedSponsorships.map((id) => stakeableSponsorships.get(id)!.payoutPerSec)) const targetsForSelected: TargetStake[] = selectedSponsorships.map((id) => [ id, - minStakePerSponsorship + payoutProportionalWei * stakeableSponsorships.get(id)!.payoutPerSec / payoutSumWeiPerSec + minStakePerSponsorship + payoutProportionalAmount * stakeableSponsorships.get(id)!.payoutPerSec / payoutPerSecSum ]) const targetsForExpired: TargetStake[] = getExpiredSponsorships(stakes, stakeableSponsorships).map((id) => [ id, @@ -140,37 +140,37 @@ export const adjustStakes: AdjustStakesFn = ({ const adjustments = [...targetStakes.keys()] .map((sponsorshipId) => ({ sponsorshipId, - differenceWei: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) + difference: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) })) - .filter(({ differenceWei: difference }) => difference !== 0n) + .filter(({ difference: difference }) => difference !== 0n) // fix rounding errors by forcing the net staking to equal unstakedAmount: adjust the largest staking - const netStakingWei = sum(adjustments.map((a) => a.differenceWei)) - if (netStakingWei !== operatorState.unstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { - const largestDifference = maxBy(adjustments, (a) => Number(a.differenceWei))! - largestDifference.differenceWei += operatorState.unstakedAmount - netStakingWei - if (largestDifference.differenceWei === 0n) { + const netStakingAmount = sum(adjustments.map((a) => a.difference)) + if (netStakingAmount !== operatorState.unstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { + const largestDifference = maxBy(adjustments, (a) => Number(a.difference))! + largestDifference.difference += operatorState.unstakedAmount - netStakingAmount + if (largestDifference.difference === 0n) { pull(adjustments, largestDifference) } } - const tooSmallAdjustments = adjustments.filter((a) => abs(a.differenceWei) < operatorConfig.minTransactionAmount) + const tooSmallAdjustments = adjustments.filter((a) => abs(a.difference) < operatorConfig.minTransactionAmount) if (tooSmallAdjustments.length > 0) { pull(adjustments, ...tooSmallAdjustments) - let netChange = sum(tooSmallAdjustments.map((a) => a.differenceWei)) - while (netChange < 0) { + let netDifference = sum(tooSmallAdjustments.map((a) => a.difference)) + while (netDifference < 0) { // there are more stakings than unstakings: remove smallest of the stakings - const smallestStaking = minBy(adjustments.filter((a) => a.differenceWei > 0), (a) => Number(a.differenceWei))! + const smallestStaking = minBy(adjustments.filter((a) => a.difference > 0), (a) => Number(a.difference))! pull(adjustments, smallestStaking) - netChange += smallestStaking.differenceWei + netDifference += smallestStaking.difference } } return sortBy( - adjustments.map(({ sponsorshipId, differenceWei }) => ({ - type: differenceWei > 0n ? 'stake' : 'unstake', + adjustments.map(({ sponsorshipId, difference }) => ({ + type: difference > 0n ? 'stake' : 'unstake', sponsorshipId, - amount: differenceWei > 0n ? differenceWei : -differenceWei + amount: difference > 0n ? difference : -difference })), (action) => ['unstake', 'stake'].indexOf(action.type) ) From 4da33207b1970c8224a58e60c1a2c0aebf8dbb8e Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 35/60] logging --- .../plugins/autostaker/AutostakerPlugin.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 33e3b231cd..0a4f4edc8a 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -61,18 +61,28 @@ export class AutostakerPlugin extends Plugin { } private async runActions(streamrClient: StreamrClient): Promise { - logger.info('Run autostaker actions') + logger.info('Run autostaker analysis') const provider = (await streamrClient.getSigner()).provider const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) .connect(provider) - const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() - const unstakedAmount = (await operatorContract.valueWithoutEarnings()) - stakedAmount - logger.info(`Balance: unstaked=${formatEther(unstakedAmount)}, staked=${formatEther(stakedAmount)}`) const stakeableSponsorships = await this.getStakeableSponsorships(streamrClient) - logger.info(`Stakeable sponsorships: ${[...stakeableSponsorships.keys()].join(',')}`) const stakes = await this.getStakes(streamrClient) - const stakeDescription = [...stakes.entries()].map(([sponsorshipId, amount]) => `${sponsorshipId}=${formatEther(amount)}`).join(', ') - logger.info(`Stakes before adjustments: ${stakeDescription}`) + const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() + const unstakedAmount = (await operatorContract.valueWithoutEarnings()) - stakedAmount + logger.debug('Analysis state', { + stakeableSponsorships: [...stakeableSponsorships.entries()].map(([sponsorshipId, config]) => ({ + sponsorshipId, + payoutPerSec: formatEther(config.payoutPerSec) + })), + stakes: [...stakes.entries()].map(([sponsorshipId, amount]) => ({ + sponsorshipId, + amount: formatEther(amount) + })), + balance: { + unstaked: formatEther(unstakedAmount), + staked: formatEther(stakedAmount) + } + }) const actions = adjustStakes({ operatorState: { stakes, From 6f0e77a7c80abcb8b34d6be1b4866242d42a459f Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 36/60] reduce logging in TheGraphClient --- packages/utils/src/TheGraphClient.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/TheGraphClient.ts b/packages/utils/src/TheGraphClient.ts index f2416335ef..96174ff31d 100644 --- a/packages/utils/src/TheGraphClient.ts +++ b/packages/utils/src/TheGraphClient.ts @@ -162,7 +162,10 @@ class IndexingState { } async waitUntilIndexed(blockNumber: number): Promise { - this.logger.debug('Wait until The Graph is synchronized', { blockTarget: blockNumber }) + if (blockNumber <= this.blockNumber) { + return + } + this.logger.debug('Wait until The Graph is synchronized', { blockNumber: this.blockNumber, blockTarget: blockNumber }) const gate = this.getOrCreateGate(blockNumber) try { await withTimeout( From 6cab147a0aae5bef580e5efa6701fa945c9cf1ba Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 09:45:54 +0300 Subject: [PATCH 37/60] rm manual test --- packages/node/bin/autostaker-manual-test.sh | 60 --------------------- 1 file changed, 60 deletions(-) delete mode 100755 packages/node/bin/autostaker-manual-test.sh diff --git a/packages/node/bin/autostaker-manual-test.sh b/packages/node/bin/autostaker-manual-test.sh deleted file mode 100755 index a405ab4331..0000000000 --- a/packages/node/bin/autostaker-manual-test.sh +++ /dev/null @@ -1,60 +0,0 @@ -# Do not merge this manual test to main - -NODE_PRIVATE_KEY="1111111111111111111111111111111111111111111111111111111111111111" -OWNER_PRIVATE_KEY="2222222222222222222222222222222222222222222222222222222222222222" -SPONSORER_PRIVATE_KEY="3333333333333333333333333333333333333333333333333333333333333333" -EARNINGS_PER_SECOND_1=1000 -EARNINGS_PER_SECOND_2=2000 -DELEGATED_AMOUNT=500000 -SPONSOR_AMOUNT=600000 - -NODE_ADDRESS=$(ethereum-address $NODE_PRIVATE_KEY | jq -r '.address') -OWNER_ADDRESS=$(ethereum-address $OWNER_PRIVATE_KEY | jq -r '.address') -SPONSORER_ADDRESS=$(ethereum-address $SPONSORER_PRIVATE_KEY | jq -r '.address') - -cd ../../cli-tools - -echo 'Mint tokens' -npx tsx bin/streamr.ts internal token-mint $NODE_ADDRESS 10000000 10000000 --env dev2 -npx tsx bin/streamr.ts internal token-mint $OWNER_ADDRESS 10000000 10000000 --env dev2 - -echo 'Create operator' -OPERATOR_CONTRACT_ADDRESS=$(npx tsx bin/streamr.ts internal operator-create -c 10 --node-addresses $NODE_ADDRESS --env dev2 --private-key $OWNER_PRIVATE_KEY | jq -r '.address') -npx tsx bin/streamr.ts internal operator-delegate $OPERATOR_CONTRACT_ADDRESS $DELEGATED_AMOUNT --env dev2 --private-key $OWNER_PRIVATE_KEY -npx tsx bin/streamr.ts internal operator-grant-controller-role $OPERATOR_CONTRACT_ADDRESS $NODE_ADDRESS --env dev2 --private-key $OWNER_PRIVATE_KEY - -echo 'Create sponsorships' -npx tsx bin/streamr.ts internal token-mint $SPONSORER_ADDRESS 10000000 10000000 --env dev2 -npx tsx bin/streamr.ts stream create /foo1 --env dev2 --private-key $SPONSORER_PRIVATE_KEY -SPONSORSHIP_CONTRACT_ADDRESS_1=$(npx tsx bin/streamr.ts internal sponsorship-create /foo1 -e $EARNINGS_PER_SECOND_1 --env dev2 --private-key $SPONSORER_PRIVATE_KEY | jq -r '.address') -npx tsx bin/streamr.ts internal sponsorship-sponsor $SPONSORSHIP_CONTRACT_ADDRESS_1 $SPONSOR_AMOUNT --env dev2 --private-key $SPONSORER_PRIVATE_KEY -npx tsx bin/streamr.ts stream create /foo2 --env dev2 --private-key $SPONSORER_PRIVATE_KEY -SPONSORSHIP_CONTRACT_ADDRESS_2=$(npx tsx bin/streamr.ts internal sponsorship-create /foo2 -e $EARNINGS_PER_SECOND_2 --env dev2 --private-key $SPONSORER_PRIVATE_KEY | jq -r '.address') -npx tsx bin/streamr.ts internal sponsorship-sponsor $SPONSORSHIP_CONTRACT_ADDRESS_2 $SPONSOR_AMOUNT --env dev2 --private-key $SPONSORER_PRIVATE_KEY - -jq -n \ - --arg nodePrivateKey "$NODE_PRIVATE_KEY" \ - --arg operatorContractAddress "$OPERATOR_CONTRACT_ADDRESS" \ - '{ - "$schema": "https://schema.streamr.network/config-v3.schema.json", - client: { - auth: { - privateKey: $nodePrivateKey - }, - environment: "dev2" - }, - plugins: { - autostaker: { - operatorContractAddress: $operatorContractAddress - } - } - }' > ../node/configs/autostaker.json - -jq -n \ - --arg operatorContract "$OPERATOR_CONTRACT_ADDRESS" \ - --arg sponsorshipContract1 "$SPONSORSHIP_CONTRACT_ADDRESS_1" \ - --arg sponsorshipContract2 "$SPONSORSHIP_CONTRACT_ADDRESS_2" \ - '$ARGS.named' - -cd ../node -npx tsx bin/streamr-node.ts configs/autostaker.json From 82dfd252cc6826c335dcf2e0d3ce28965a584e2a Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 15:07:29 +0300 Subject: [PATCH 38/60] rm ethereum-private-key addition (not needed as we don't configure private key) --- packages/node/src/config/validateConfig.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node/src/config/validateConfig.ts b/packages/node/src/config/validateConfig.ts index 30fce45d09..8560c650e8 100644 --- a/packages/node/src/config/validateConfig.ts +++ b/packages/node/src/config/validateConfig.ts @@ -9,7 +9,6 @@ export const validateConfig = (data: unknown, schema: Schema, contextName?: stri }) addFormats(ajv) ajv.addFormat('ethereum-address', /^0x[a-zA-Z0-9]{40}$/) - ajv.addFormat('ethereum-private-key', /^(0x)?[a-zA-Z0-9]{64}$/) ajv.addSchema(DEFINITIONS_SCHEMA) if (!ajv.validate(schema, data)) { const prefix = (contextName !== undefined) ? (contextName + ': ') : '' From 03a30779f0312456c8cf760f3a6ac144fc8f0ad9 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Mon, 2 Jun 2025 16:31:55 +0300 Subject: [PATCH 39/60] rm unnecessary filter in getStakeableSponsorships() --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 0a4f4edc8a..929ec47f9d 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -111,16 +111,13 @@ export class AutostakerPlugin extends Plugin { // eslint-disable-next-line class-methods-use-this private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { - // TODO what are good where conditions for the sponsorships query so that we get all stakeable sponsorships - // but no non-stakables (e.g. expired) const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { return { query: ` { sponsorships( where: { - projectedInsolvency_gt: ${Math.floor(Date.now() / 1000)}, - spotAPY_gt: 0 + projectedInsolvency_gt: ${Math.floor(Date.now() / 1000)} id_gt: "${lastId}" }, first: ${pageSize} From 0a4331517a668a324bc2fd2bf0f469a72ee09343 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 3 Jun 2025 14:24:58 +0300 Subject: [PATCH 40/60] improve logging --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 929ec47f9d..645e4da5ed 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -100,7 +100,7 @@ export class AutostakerPlugin extends Plugin { }) const signer = await streamrClient.getSigner() for (const action of actions) { - logger.info(`Action: ${action.type} ${formatEther(action.amount)} ${action.sponsorshipId}`) + logger.info(`Execute action: ${action.type} ${formatEther(action.amount)} ${action.sponsorshipId}`) await getStakeOrUnstakeFunction(action)(signer, this.pluginConfig.operatorContractAddress, action.sponsorshipId, From 2e7fa1504643d540ffe499ec73726453bc9be43f Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 3 Jun 2025 14:37:06 +0300 Subject: [PATCH 41/60] fetchMinStakePerSponsorship() --- .../plugins/autostaker/AutostakerPlugin.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 645e4da5ed..48c72f115e 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -29,6 +29,19 @@ interface StakeQueryResultItem { const logger = new Logger(module) +const fetchMinStakePerSponsorship = async (theGraphClient: TheGraphClient): Promise => { + const queryResult = await theGraphClient.queryEntity<{ network: { minimumStakeWei: string } }>({ + query: ` + { + network (id: "network-entity-id") { + minimumStakeWei + } + } + ` + }) + return BigInt(queryResult.network.minimumStakeWei) +} + const getStakeOrUnstakeFunction = (action: Action): ( operatorOwnerWallet: SignerWithProvider, operatorContractAddress: string, @@ -51,16 +64,17 @@ export class AutostakerPlugin extends Plugin { async start(streamrClient: StreamrClient): Promise { logger.info('Start autostaker plugin') + const minStakePerSponsorship = await fetchMinStakePerSponsorship(streamrClient.getTheGraphClient()) scheduleAtApproximateInterval(async () => { try { - await this.runActions(streamrClient) + await this.runActions(streamrClient, minStakePerSponsorship) } catch (err) { logger.warn('Error while running autostaker actions', { err }) } }, this.pluginConfig.runIntervalInMs, 0.1, false, this.abortController.signal) } - private async runActions(streamrClient: StreamrClient): Promise { + private async runActions(streamrClient: StreamrClient, minStakePerSponsorship: bigint): Promise { logger.info('Run autostaker analysis') const provider = (await streamrClient.getSigner()).provider const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) @@ -95,7 +109,7 @@ export class AutostakerPlugin extends Plugin { }, stakeableSponsorships, environmentConfig: { - minStakePerSponsorship: 5000000000000000000000n // TODO read from The Graph (network.minimumStakeWei) + minStakePerSponsorship } }) const signer = await streamrClient.getSigner() From 95facd5b641720e427cae1d28b118ee6dc1bbbf8 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 3 Jun 2025 14:37:13 +0300 Subject: [PATCH 42/60] style --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 48c72f115e..a4b4349c65 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -1,5 +1,5 @@ import { _operatorContractUtils, SignerWithProvider, StreamrClient } from '@streamr/sdk' -import { collect, Logger, scheduleAtApproximateInterval, WeiAmount } from '@streamr/utils' +import { collect, Logger, scheduleAtApproximateInterval, TheGraphClient, WeiAmount } from '@streamr/utils' import { Schema } from 'ajv' import { formatEther, parseEther } from 'ethers' import { Plugin } from '../../Plugin' @@ -129,7 +129,7 @@ export class AutostakerPlugin extends Plugin { return { query: ` { - sponsorships( + sponsorships ( where: { projectedInsolvency_gt: ${Math.floor(Date.now() / 1000)} id_gt: "${lastId}" @@ -156,7 +156,7 @@ export class AutostakerPlugin extends Plugin { return { query: ` { - stakes( + stakes ( where: { operator: "${this.pluginConfig.operatorContractAddress.toLowerCase()}", id_gt: "${lastId}" From 84775aead70b92a5e59f8ef8d22d2769948482c2 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 3 Jun 2025 14:42:41 +0300 Subject: [PATCH 43/60] move sum() to payoutProportionalStrategy.ts --- .../src/plugins/autostaker/payoutProportionalStrategy.ts | 6 +++++- packages/utils/src/exports.ts | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 52833a16f9..7361096e7d 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -1,4 +1,4 @@ -import { sum, WeiAmount } from '@streamr/utils' +import { WeiAmount } from '@streamr/utils' import crypto from 'crypto' import maxBy from 'lodash/maxBy' import minBy from 'lodash/minBy' @@ -41,6 +41,10 @@ import { Action, AdjustStakesFn, SponsorshipConfig, SponsorshipID } from './type type TargetStake = [SponsorshipID, WeiAmount] +const sum = (values: bigint[]): bigint =>{ + return values.reduce((acc, value) => acc + value, 0n) +} + const abs = (n: bigint) => (n < 0n) ? -n : n const getExpiredSponsorships = ( diff --git a/packages/utils/src/exports.ts b/packages/utils/src/exports.ts index b60680fdf6..a322f49558 100644 --- a/packages/utils/src/exports.ts +++ b/packages/utils/src/exports.ts @@ -55,4 +55,3 @@ export type { ChangeFieldType, MapKey } from './types' export { type WeiAmount, multiplyWeiAmount } from './WeiAmount' export { getSubtle } from './crossPlatformCrypto' export { SigningUtil, EcdsaSecp256k1Evm, EcdsaSecp256r1, MlDsa87, type KeyType, KEY_TYPES } from './SigningUtil' -export { sum } from './sum' From 70cf31d9161a18045e8f2ff772a366e176ffbbc5 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Wed, 4 Jun 2025 23:57:41 +0300 Subject: [PATCH 44/60] minimumStakingPeriodSeconds --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index a4b4349c65..310fa9d0fd 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -126,12 +126,15 @@ export class AutostakerPlugin extends Plugin { // eslint-disable-next-line class-methods-use-this private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { + // TODO add support spnsorships which have non-zero minimumStakingPeriodSeconds (i.e. implement some loggic in the + // payoutPropotionalStrategy so that we ensure that unstaking doesn't happen too soon) return { query: ` { sponsorships ( where: { projectedInsolvency_gt: ${Math.floor(Date.now() / 1000)} + minimumStakingPeriodSeconds: "0" id_gt: "${lastId}" }, first: ${pageSize} From 44028c28d7d6da60164b47e318920eddb52ed9ab Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 00:20:46 +0300 Subject: [PATCH 45/60] maxAcceptableMinOperatorCount --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 3 ++- packages/node/src/plugins/autostaker/config.schema.json | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 310fa9d0fd..2b94de48c8 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -11,6 +11,7 @@ export interface AutostakerPluginConfig { operatorContractAddress: string runIntervalInMs: number minTransactionDataTokenAmount: number + maxAcceptableMinOperatorCount: number maxSponsorshipCount?: number } @@ -123,7 +124,6 @@ export class AutostakerPlugin extends Plugin { } } - // eslint-disable-next-line class-methods-use-this private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { // TODO add support spnsorships which have non-zero minimumStakingPeriodSeconds (i.e. implement some loggic in the @@ -135,6 +135,7 @@ export class AutostakerPlugin extends Plugin { where: { projectedInsolvency_gt: ${Math.floor(Date.now() / 1000)} minimumStakingPeriodSeconds: "0" + minOperators_lte: ${this.pluginConfig.maxAcceptableMinOperatorCount} id_gt: "${lastId}" }, first: ${pageSize} diff --git a/packages/node/src/plugins/autostaker/config.schema.json b/packages/node/src/plugins/autostaker/config.schema.json index 07c58d1d15..060abb9d0d 100644 --- a/packages/node/src/plugins/autostaker/config.schema.json +++ b/packages/node/src/plugins/autostaker/config.schema.json @@ -25,6 +25,12 @@ "minimum": 0, "default": 1000 }, + "maxAcceptableMinOperatorCount": { + "type": "integer", + "description": "Maximum acceptable value for a sponsorship's minOperatorCount config option", + "minimum": 0, + "default": 100 + }, "maxSponsorshipCount": { "type": "integer", "description": "Maximum count of sponsorships which are staked at any given time", From 6b53523e52d49e8d9c800b94e8db5295911beb6d Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 00:53:02 +0300 Subject: [PATCH 46/60] maxOperators --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 2b94de48c8..33a28b171b 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -18,6 +18,8 @@ export interface AutostakerPluginConfig { interface SponsorshipQueryResultItem { id: SponsorshipID totalPayoutWeiPerSec: WeiAmount + operatorCount: number + maxOperators: number | null } interface StakeQueryResultItem { @@ -142,13 +144,18 @@ export class AutostakerPlugin extends Plugin { ) { id totalPayoutWeiPerSec + operatorCount + maxOperators } } ` } }) const sponsorships = await collect(queryResult) - return new Map(sponsorships.map( + const hasAcceptableOperatorCount = (item: SponsorshipQueryResultItem) => { + return (item.maxOperators === null) || (item.operatorCount < item.maxOperators) + } + return new Map(sponsorships.filter(hasAcceptableOperatorCount).map( (sponsorship) => [sponsorship.id, { payoutPerSec: BigInt(sponsorship.totalPayoutWeiPerSec), }]) From b85a3d2581120099b9b321c9f3f3c2b6f578ac19 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 12:30:22 +0300 Subject: [PATCH 47/60] MIN_SPONSORSHIP_TOTAL_PAYOUT_PER_SECOND --- packages/node/src/plugins/autostaker/AutostakerPlugin.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 33a28b171b..176808e265 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -32,6 +32,9 @@ interface StakeQueryResultItem { const logger = new Logger(module) +// 1e12 wei, i.e. one millionth of one DATA token (we can tweak this later if needed) +const MIN_SPONSORSHIP_TOTAL_PAYOUT_PER_SECOND = 1000000000000n + const fetchMinStakePerSponsorship = async (theGraphClient: TheGraphClient): Promise => { const queryResult = await theGraphClient.queryEntity<{ network: { minimumStakeWei: string } }>({ query: ` @@ -138,6 +141,7 @@ export class AutostakerPlugin extends Plugin { projectedInsolvency_gt: ${Math.floor(Date.now() / 1000)} minimumStakingPeriodSeconds: "0" minOperators_lte: ${this.pluginConfig.maxAcceptableMinOperatorCount} + totalPayoutWeiPerSec_gte: "${MIN_SPONSORSHIP_TOTAL_PAYOUT_PER_SECOND.toString()}" id_gt: "${lastId}" }, first: ${pageSize} From 9a84781a2de8293185c4b61b24e9f7200f6d5a7a Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 13:18:31 +0300 Subject: [PATCH 48/60] improve tooSmallAdjustments removal --- .../autostaker/payoutProportionalStrategy.ts | 17 ++++++++---- .../payoutProportionalStrategy.test.ts | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 7361096e7d..3a33f1af7c 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -162,11 +162,18 @@ export const adjustStakes: AdjustStakesFn = ({ if (tooSmallAdjustments.length > 0) { pull(adjustments, ...tooSmallAdjustments) let netDifference = sum(tooSmallAdjustments.map((a) => a.difference)) - while (netDifference < 0) { - // there are more stakings than unstakings: remove smallest of the stakings - const smallestStaking = minBy(adjustments.filter((a) => a.difference > 0), (a) => Number(a.difference))! - pull(adjustments, smallestStaking) - netDifference += smallestStaking.difference + while (true) { + const stakings = adjustments.filter((a) => a.difference > 0) + const unstakings = adjustments.filter((a) => a.difference < 0) + const stakingSum = sum(stakings.map((a) => a.difference)) + const availableSum = abs(sum(unstakings.map((a) => a.difference))) + operatorState.unstakedAmount + if (stakingSum > availableSum) { + const smallestStaking = minBy(stakings, (a) => Number(a.difference))! + pull(adjustments, smallestStaking) + netDifference += smallestStaking.difference + } else { + break + } } } diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index d555472c1c..81c99805fa 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -286,5 +286,31 @@ describe('payoutProportionalStrategy', () => { environmentConfig: { minStakePerSponsorship: 0n } })).toEqual([]) }) + + it('very small expiration unstake and nothing to stake', () => { + expect(adjustStakes({ + operatorState: { unstakedAmount: 0n, stakes: new Map([ + ['a', 10n], + ['b', 1000n] + ]) }, + operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, + stakeableSponsorships: new Map([]), + environmentConfig: { minStakePerSponsorship: 0n } + })).toEqual([ + { type: 'unstake', sponsorshipId: 'b', amount: 1000n } + ]) + }) + + it('only multiple very small expiration unstakes', () => { + expect(adjustStakes({ + operatorState: { unstakedAmount: 0n, stakes: new Map([ + ['a', 10n], + ['b', 20n] + ]) }, + operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, + stakeableSponsorships: new Map([]), + environmentConfig: { minStakePerSponsorship: 0n } + })).toEqual([]) + }) }) }) From 5ddf14ffae1ab71ccc41fb40bae7d96f1201dbf5 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 13:30:55 +0300 Subject: [PATCH 49/60] rm unnecessary variable --- .../node/src/plugins/autostaker/payoutProportionalStrategy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 3a33f1af7c..ddae6f08a0 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -161,7 +161,6 @@ export const adjustStakes: AdjustStakesFn = ({ const tooSmallAdjustments = adjustments.filter((a) => abs(a.difference) < operatorConfig.minTransactionAmount) if (tooSmallAdjustments.length > 0) { pull(adjustments, ...tooSmallAdjustments) - let netDifference = sum(tooSmallAdjustments.map((a) => a.difference)) while (true) { const stakings = adjustments.filter((a) => a.difference > 0) const unstakings = adjustments.filter((a) => a.difference < 0) @@ -170,7 +169,6 @@ export const adjustStakes: AdjustStakesFn = ({ if (stakingSum > availableSum) { const smallestStaking = minBy(stakings, (a) => Number(a.difference))! pull(adjustments, smallestStaking) - netDifference += smallestStaking.difference } else { break } From 3ddfc92ceb974aaa1495c3c032093d9005826651 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 13:33:00 +0300 Subject: [PATCH 50/60] add comment --- .../node/src/plugins/autostaker/payoutProportionalStrategy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index ddae6f08a0..42cb64d660 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -125,6 +125,10 @@ const getTargetStakes = ( return new Map([...targetsForSelected, ...targetsForExpired]) } +/** + * @returns A list of stake and unstake actions. The actions should be ordered so that transactions can be executed sequentially, + * e.g. all unstake actions first to ensure sufficient balance for the subsequent staking actions. + */ export const adjustStakes: AdjustStakesFn = ({ operatorState, operatorConfig, From a1c87b67c406781569275be2a4c12e966ebae758 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 14:04:00 +0300 Subject: [PATCH 51/60] rm unnecessary Number -> bigint type conversions --- .../autostaker/payoutProportionalStrategy.ts | 8 ++++---- .../autostaker/payoutProportionalStrategy.test.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 42cb64d660..5f9f4ee73a 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -67,7 +67,7 @@ const getSelectedSponsorships = ( const count = Math.min( stakeableSponsorships.size, maxSponsorshipCount ?? Infinity, - Math.floor(Number(totalStakeableAmount) / Number(minStakePerSponsorship)) // as many as we can afford + (minStakePerSponsorship > 0n) ? Number(totalStakeableAmount / minStakePerSponsorship) : Infinity // as many as we can afford ) const [ keptSponsorships, @@ -76,7 +76,7 @@ const getSelectedSponsorships = ( return [ ...keptSponsorships, ...sortBy(potentialSponsorships, - (id) => -Number(stakeableSponsorships.get(id)!.payoutPerSec), + (id) => -stakeableSponsorships.get(id)!.payoutPerSec, (id) => { // If payoutPerSec is same for multiple sponsorships, different operators should // choose different sponsorships. Using hash of some operator-specific ID + sponsorshipId @@ -155,7 +155,7 @@ export const adjustStakes: AdjustStakesFn = ({ // fix rounding errors by forcing the net staking to equal unstakedAmount: adjust the largest staking const netStakingAmount = sum(adjustments.map((a) => a.difference)) if (netStakingAmount !== operatorState.unstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { - const largestDifference = maxBy(adjustments, (a) => Number(a.difference))! + const largestDifference = maxBy(adjustments, (a) => a.difference)! largestDifference.difference += operatorState.unstakedAmount - netStakingAmount if (largestDifference.difference === 0n) { pull(adjustments, largestDifference) @@ -171,7 +171,7 @@ export const adjustStakes: AdjustStakesFn = ({ const stakingSum = sum(stakings.map((a) => a.difference)) const availableSum = abs(sum(unstakings.map((a) => a.difference))) + operatorState.unstakedAmount if (stakingSum > availableSum) { - const smallestStaking = minBy(stakings, (a) => Number(a.difference))! + const smallestStaking = minBy(stakings, (a) => a.difference)! pull(adjustments, smallestStaking) } else { break diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 81c99805fa..ee58d64fb3 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -1,3 +1,4 @@ +import sortBy from 'lodash/sortBy' import { adjustStakes } from '../../../../src/plugins/autostaker/payoutProportionalStrategy' describe('payoutProportionalStrategy', () => { @@ -192,6 +193,20 @@ describe('payoutProportionalStrategy', () => { })).toHaveLength(4) }) + it('handles greater than MAX_SAFE_INTEGER payout values correctly', () => { + const stakes = adjustStakes({ + operatorState: { unstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), stakes: new Map() }, + operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, + stakeableSponsorships: new Map([ + ['a', { payoutPerSec: 3n * BigInt(Number.MAX_SAFE_INTEGER) }], + ['b', { payoutPerSec: 4n * BigInt(Number.MAX_SAFE_INTEGER) }], + ['c', { payoutPerSec: 2n * BigInt(Number.MAX_SAFE_INTEGER) }], + ]), + environmentConfig: { minStakePerSponsorship: 0n }, + }) + expect(sortBy(stakes, (a) => a.amount).map((a) => a.sponsorshipId)).toEqual(['c', 'a', 'b']) + }) + it('operators may choose different sponsorships if payoutPerSec are same', () => { const createArgs = (operatorContractAddress: string) => { return { From 9649cba2c388aa169fb8a5f5e99c4c3ab0df86d4 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 15:23:24 +0300 Subject: [PATCH 52/60] fix hasAcceptableOperatorCount() for sponsorships which we've already staked to --- .../src/plugins/autostaker/AutostakerPlugin.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 176808e265..e4ab915fac 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -85,8 +85,8 @@ export class AutostakerPlugin extends Plugin { const provider = (await streamrClient.getSigner()).provider const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) .connect(provider) - const stakeableSponsorships = await this.getStakeableSponsorships(streamrClient) const stakes = await this.getStakes(streamrClient) + const stakeableSponsorships = await this.getStakeableSponsorships(stakes, streamrClient) const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() const unstakedAmount = (await operatorContract.valueWithoutEarnings()) - stakedAmount logger.debug('Analysis state', { @@ -129,7 +129,10 @@ export class AutostakerPlugin extends Plugin { } } - private async getStakeableSponsorships(streamrClient: StreamrClient): Promise> { + private async getStakeableSponsorships( + stakes: Map, + streamrClient: StreamrClient + ): Promise> { const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { // TODO add support spnsorships which have non-zero minimumStakingPeriodSeconds (i.e. implement some loggic in the // payoutPropotionalStrategy so that we ensure that unstaking doesn't happen too soon) @@ -157,7 +160,13 @@ export class AutostakerPlugin extends Plugin { }) const sponsorships = await collect(queryResult) const hasAcceptableOperatorCount = (item: SponsorshipQueryResultItem) => { - return (item.maxOperators === null) || (item.operatorCount < item.maxOperators) + if (stakes.has(item.id)) { + // this operator has already staked to the sponsorship: keep the sponsorship in the list so that + // we don't unstake from it + return true + } else { + return (item.maxOperators === null) || (item.operatorCount < item.maxOperators) + } } return new Map(sponsorships.filter(hasAcceptableOperatorCount).map( (sponsorship) => [sponsorship.id, { From 7caa8f07a10bb6c83f887a2d2802653fdd679968 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 15:46:07 +0300 Subject: [PATCH 53/60] always unstake from expired sponsorship --- .../src/plugins/autostaker/payoutProportionalStrategy.ts | 5 ++++- .../plugins/autostaker/payoutProportionalStrategy.test.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 5f9f4ee73a..02116cf3fb 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -162,7 +162,10 @@ export const adjustStakes: AdjustStakesFn = ({ } } - const tooSmallAdjustments = adjustments.filter((a) => abs(a.difference) < operatorConfig.minTransactionAmount) + const tooSmallAdjustments = adjustments.filter( + // note the edge case: expired sponsorships can be unstaked, even if the transaction amount is considered "too small" + (a) => (abs(a.difference) < operatorConfig.minTransactionAmount) && stakeableSponsorships.has(a.sponsorshipId) + ) if (tooSmallAdjustments.length > 0) { pull(adjustments, ...tooSmallAdjustments) while (true) { diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index ee58d64fb3..b1977f0557 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -311,7 +311,8 @@ describe('payoutProportionalStrategy', () => { operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([]), environmentConfig: { minStakePerSponsorship: 0n } - })).toEqual([ + })).toIncludeSameMembers([ + { type: 'unstake', sponsorshipId: 'a', amount: 10n }, { type: 'unstake', sponsorshipId: 'b', amount: 1000n } ]) }) @@ -325,7 +326,10 @@ describe('payoutProportionalStrategy', () => { operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([]), environmentConfig: { minStakePerSponsorship: 0n } - })).toEqual([]) + })).toIncludeSameMembers([ + { type: 'unstake', sponsorshipId: 'a', amount: 10n }, + { type: 'unstake', sponsorshipId: 'b', amount: 20n } + ]) }) }) }) From 645a1abc1c5dbf3f385418b1a5cbf632b08b199d Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 15:54:34 +0300 Subject: [PATCH 54/60] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ca532503..58dbe37ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ Changes before Tatum release are not documented in this file. #### Added +- Add experimental `autostaker` plugin that manages sponsorship staking and unstaking automatically for operators (https://github.com/streamr-dev/network/pull/3086) + #### Changed - **BREAKING CHANGE**: Node.js v20 or higher is required (https://github.com/streamr-dev/network/pull/3138) From 29cd6ced0c273a9e80381938126772af0a4817b7 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 18:47:06 +0300 Subject: [PATCH 55/60] rename stakes -> myCurrentStakes --- .../plugins/autostaker/AutostakerPlugin.ts | 10 ++--- .../autostaker/payoutProportionalStrategy.ts | 20 +++++----- packages/node/src/plugins/autostaker/types.ts | 2 +- .../payoutProportionalStrategy.test.ts | 40 +++++++++---------- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index e4ab915fac..bdf7e24e12 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -85,8 +85,8 @@ export class AutostakerPlugin extends Plugin { const provider = (await streamrClient.getSigner()).provider const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) .connect(provider) - const stakes = await this.getStakes(streamrClient) - const stakeableSponsorships = await this.getStakeableSponsorships(stakes, streamrClient) + const myCurrentStakes = await this.getMyCurrentStakes(streamrClient) + const stakeableSponsorships = await this.getStakeableSponsorships(myCurrentStakes, streamrClient) const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() const unstakedAmount = (await operatorContract.valueWithoutEarnings()) - stakedAmount logger.debug('Analysis state', { @@ -94,7 +94,7 @@ export class AutostakerPlugin extends Plugin { sponsorshipId, payoutPerSec: formatEther(config.payoutPerSec) })), - stakes: [...stakes.entries()].map(([sponsorshipId, amount]) => ({ + myCurrentStakes: [...myCurrentStakes.entries()].map(([sponsorshipId, amount]) => ({ sponsorshipId, amount: formatEther(amount) })), @@ -105,7 +105,7 @@ export class AutostakerPlugin extends Plugin { }) const actions = adjustStakes({ operatorState: { - stakes, + myCurrentStakes, unstakedAmount }, operatorConfig: { @@ -175,7 +175,7 @@ export class AutostakerPlugin extends Plugin { ) } - private async getStakes(streamrClient: StreamrClient): Promise> { + private async getMyCurrentStakes(streamrClient: StreamrClient): Promise> { const queryResult = streamrClient.getTheGraphClient().queryEntities((lastId: string, pageSize: number) => { return { query: ` diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 02116cf3fb..bd807f56b9 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -48,16 +48,16 @@ const sum = (values: bigint[]): bigint =>{ const abs = (n: bigint) => (n < 0n) ? -n : n const getExpiredSponsorships = ( - stakes: Map, + myCurrentStakes: Map, stakeableSponsorships: Map ): SponsorshipID[] => { - return [...stakes.keys()].filter((sponsorshipId) => !stakeableSponsorships.has(sponsorshipId)) + return [...myCurrentStakes.keys()].filter((sponsorshipId) => !stakeableSponsorships.has(sponsorshipId)) } /* * Select sponsorships for which we should have some stake */ const getSelectedSponsorships = ( - stakes: Map, + myCurrentStakes: Map, stakeableSponsorships: Map, totalStakeableAmount: WeiAmount, minStakePerSponsorship: WeiAmount, @@ -72,7 +72,7 @@ const getSelectedSponsorships = ( const [ keptSponsorships, potentialSponsorships, - ] = partition([...stakeableSponsorships.keys()], (id) => stakes.has(id)) + ] = partition([...stakeableSponsorships.keys()], (id) => myCurrentStakes.has(id)) return [ ...keptSponsorships, ...sortBy(potentialSponsorships, @@ -95,16 +95,16 @@ const getSelectedSponsorships = ( * - for expired sponsorships the stake is zero */ const getTargetStakes = ( - stakes: Map, + myCurrentStakes: Map, stakeableSponsorships: Map, unstakedAmount: WeiAmount, minStakePerSponsorship: WeiAmount, maxSponsorshipCount: number | undefined, operatorContractAddress: string ): Map => { - const totalStakeableAmount = sum([...stakes.values()]) + unstakedAmount + const totalStakeableAmount = sum([...myCurrentStakes.values()]) + unstakedAmount const selectedSponsorships = getSelectedSponsorships( - stakes, + myCurrentStakes, stakeableSponsorships, totalStakeableAmount, minStakePerSponsorship, @@ -118,7 +118,7 @@ const getTargetStakes = ( id, minStakePerSponsorship + payoutProportionalAmount * stakeableSponsorships.get(id)!.payoutPerSec / payoutPerSecSum ]) - const targetsForExpired: TargetStake[] = getExpiredSponsorships(stakes, stakeableSponsorships).map((id) => [ + const targetsForExpired: TargetStake[] = getExpiredSponsorships(myCurrentStakes, stakeableSponsorships).map((id) => [ id, 0n ]) @@ -137,7 +137,7 @@ export const adjustStakes: AdjustStakesFn = ({ }): Action[] => { const targetStakes = getTargetStakes( - operatorState.stakes, + operatorState.myCurrentStakes, stakeableSponsorships, operatorState.unstakedAmount, environmentConfig.minStakePerSponsorship, @@ -148,7 +148,7 @@ export const adjustStakes: AdjustStakesFn = ({ const adjustments = [...targetStakes.keys()] .map((sponsorshipId) => ({ sponsorshipId, - difference: targetStakes.get(sponsorshipId)! - (operatorState.stakes.get(sponsorshipId) ?? 0n) + difference: targetStakes.get(sponsorshipId)! - (operatorState.myCurrentStakes.get(sponsorshipId) ?? 0n) })) .filter(({ difference: difference }) => difference !== 0n) diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 3eb3761618..2b1e610579 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -23,7 +23,7 @@ export interface SponsorshipConfig { } export interface OperatorState { - stakes: Map + myCurrentStakes: Map unstakedAmount: WeiAmount } diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index b1977f0557..f30b14d0bb 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -5,7 +5,7 @@ describe('payoutProportionalStrategy', () => { it('stake all', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 11000n, stakes: new Map() }, + operatorState: { unstakedAmount: 11000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 2n }], @@ -21,7 +21,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, stakes: new Map([[ 'a', 2000n ]]) }, + operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map([[ 'a', 2000n ]]) }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), environmentConfig: { minStakePerSponsorship: 1234n }, @@ -32,7 +32,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 600n, stakes: new Map() }, + operatorState: { unstakedAmount: 600n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -49,7 +49,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 500n, stakes: new Map() }, + operatorState: { unstakedAmount: 500n, myCurrentStakes: new Map() }, operatorConfig: { maxSponsorshipCount: 2, minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -65,7 +65,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to minStakePerSponsorship and available tokens', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 500n, stakes: new Map() }, + operatorState: { unstakedAmount: 500n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -80,7 +80,7 @@ describe('payoutProportionalStrategy', () => { it('doesn\'t allocate tokens if less available than minimum stake', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 100n, stakes: new Map() }, + operatorState: { unstakedAmount: 100n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), environmentConfig: { minStakePerSponsorship: 300n }, @@ -90,7 +90,7 @@ describe('payoutProportionalStrategy', () => { // unstakes must happen first because otherwise there isn't enough tokens for staking it('sends out unstakes before stakes', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ [ 'a', 30n ], [ 'b', 70n ], ]) }, @@ -113,7 +113,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes from expired sponsorships', async () => { // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, stakes: new Map([[ 'b', 100n ]]) }, + operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([[ 'b', 100n ]]) }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -127,7 +127,7 @@ describe('payoutProportionalStrategy', () => { it('restakes expired sponsorship stakes into other sponsorships', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ [ 'a', 100n ], [ 'b', 100n ], [ 'c', 100n ], @@ -147,7 +147,7 @@ describe('payoutProportionalStrategy', () => { it('handles rounding errors', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -164,7 +164,7 @@ describe('payoutProportionalStrategy', () => { it('rounding error no-op case', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ ['a', 166n ], ['b', 166n ], ['c', 668n ], @@ -181,7 +181,7 @@ describe('payoutProportionalStrategy', () => { it('uses Infinity as default maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -195,7 +195,7 @@ describe('payoutProportionalStrategy', () => { it('handles greater than MAX_SAFE_INTEGER payout values correctly', () => { const stakes = adjustStakes({ - operatorState: { unstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), stakes: new Map() }, + operatorState: { unstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 3n * BigInt(Number.MAX_SAFE_INTEGER) }], @@ -210,7 +210,7 @@ describe('payoutProportionalStrategy', () => { it('operators may choose different sponsorships if payoutPerSec are same', () => { const createArgs = (operatorContractAddress: string) => { return { - operatorState: { unstakedAmount: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -231,7 +231,7 @@ describe('payoutProportionalStrategy', () => { describe('exclude small transactions', () => { it('exclude small stakings', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, stakes: new Map() }, + operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -246,7 +246,7 @@ describe('payoutProportionalStrategy', () => { it('one small transaction is balanced by removing one staking', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 820n, stakes: new Map([ + operatorState: { unstakedAmount: 820n, myCurrentStakes: new Map([ ['a', 180n] ]) }, operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, @@ -263,7 +263,7 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing multiple stakings', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 740n, stakes: new Map([ + operatorState: { unstakedAmount: 740n, myCurrentStakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n] @@ -284,7 +284,7 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing all stakings', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 359n, stakes: new Map([ + operatorState: { unstakedAmount: 359n, myCurrentStakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n], @@ -304,7 +304,7 @@ describe('payoutProportionalStrategy', () => { it('very small expiration unstake and nothing to stake', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ ['a', 10n], ['b', 1000n] ]) }, @@ -319,7 +319,7 @@ describe('payoutProportionalStrategy', () => { it('only multiple very small expiration unstakes', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, stakes: new Map([ + operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ ['a', 10n], ['b', 20n] ]) }, From a51bbc7ec9723836a69cd2774300987ce457ec42 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 19:49:46 +0300 Subject: [PATCH 56/60] rename unstakedAmount -> myUnstakedAmount --- .../plugins/autostaker/AutostakerPlugin.ts | 10 ++--- .../autostaker/payoutProportionalStrategy.ts | 14 +++---- packages/node/src/plugins/autostaker/types.ts | 2 +- .../payoutProportionalStrategy.test.ts | 40 +++++++++---------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index bdf7e24e12..796000ccd8 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -87,8 +87,8 @@ export class AutostakerPlugin extends Plugin { .connect(provider) const myCurrentStakes = await this.getMyCurrentStakes(streamrClient) const stakeableSponsorships = await this.getStakeableSponsorships(myCurrentStakes, streamrClient) - const stakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() - const unstakedAmount = (await operatorContract.valueWithoutEarnings()) - stakedAmount + const myStakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() + const myUnstakedAmount = (await operatorContract.valueWithoutEarnings()) - myStakedAmount logger.debug('Analysis state', { stakeableSponsorships: [...stakeableSponsorships.entries()].map(([sponsorshipId, config]) => ({ sponsorshipId, @@ -99,14 +99,14 @@ export class AutostakerPlugin extends Plugin { amount: formatEther(amount) })), balance: { - unstaked: formatEther(unstakedAmount), - staked: formatEther(stakedAmount) + unstaked: formatEther(myUnstakedAmount), + staked: formatEther(myStakedAmount) } }) const actions = adjustStakes({ operatorState: { myCurrentStakes, - unstakedAmount + myUnstakedAmount }, operatorConfig: { operatorContractAddress: this.pluginConfig.operatorContractAddress, diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index bd807f56b9..dc4b030490 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -97,12 +97,12 @@ const getSelectedSponsorships = ( const getTargetStakes = ( myCurrentStakes: Map, stakeableSponsorships: Map, - unstakedAmount: WeiAmount, + myUnstakedAmount: WeiAmount, minStakePerSponsorship: WeiAmount, maxSponsorshipCount: number | undefined, operatorContractAddress: string ): Map => { - const totalStakeableAmount = sum([...myCurrentStakes.values()]) + unstakedAmount + const totalStakeableAmount = sum([...myCurrentStakes.values()]) + myUnstakedAmount const selectedSponsorships = getSelectedSponsorships( myCurrentStakes, stakeableSponsorships, @@ -139,7 +139,7 @@ export const adjustStakes: AdjustStakesFn = ({ const targetStakes = getTargetStakes( operatorState.myCurrentStakes, stakeableSponsorships, - operatorState.unstakedAmount, + operatorState.myUnstakedAmount, environmentConfig.minStakePerSponsorship, operatorConfig.maxSponsorshipCount, operatorConfig.operatorContractAddress @@ -152,11 +152,11 @@ export const adjustStakes: AdjustStakesFn = ({ })) .filter(({ difference: difference }) => difference !== 0n) - // fix rounding errors by forcing the net staking to equal unstakedAmount: adjust the largest staking + // fix rounding errors by forcing the net staking to equal myUnstakedAmount: adjust the largest staking const netStakingAmount = sum(adjustments.map((a) => a.difference)) - if (netStakingAmount !== operatorState.unstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { + if (netStakingAmount !== operatorState.myUnstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { const largestDifference = maxBy(adjustments, (a) => a.difference)! - largestDifference.difference += operatorState.unstakedAmount - netStakingAmount + largestDifference.difference += operatorState.myUnstakedAmount - netStakingAmount if (largestDifference.difference === 0n) { pull(adjustments, largestDifference) } @@ -172,7 +172,7 @@ export const adjustStakes: AdjustStakesFn = ({ const stakings = adjustments.filter((a) => a.difference > 0) const unstakings = adjustments.filter((a) => a.difference < 0) const stakingSum = sum(stakings.map((a) => a.difference)) - const availableSum = abs(sum(unstakings.map((a) => a.difference))) + operatorState.unstakedAmount + const availableSum = abs(sum(unstakings.map((a) => a.difference))) + operatorState.myUnstakedAmount if (stakingSum > availableSum) { const smallestStaking = minBy(stakings, (a) => a.difference)! pull(adjustments, smallestStaking) diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 2b1e610579..606fdb9e89 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -24,7 +24,7 @@ export interface SponsorshipConfig { export interface OperatorState { myCurrentStakes: Map - unstakedAmount: WeiAmount + myUnstakedAmount: WeiAmount } export interface OperatorConfig { diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index f30b14d0bb..af82187830 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -5,7 +5,7 @@ describe('payoutProportionalStrategy', () => { it('stake all', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 11000n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 11000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 2n }], @@ -21,7 +21,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map([[ 'a', 2000n ]]) }, + operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map([[ 'a', 2000n ]]) }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), environmentConfig: { minStakePerSponsorship: 1234n }, @@ -32,7 +32,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 600n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 600n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -49,7 +49,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 500n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 500n, myCurrentStakes: new Map() }, operatorConfig: { maxSponsorshipCount: 2, minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -65,7 +65,7 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to minStakePerSponsorship and available tokens', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 500n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 500n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -80,7 +80,7 @@ describe('payoutProportionalStrategy', () => { it('doesn\'t allocate tokens if less available than minimum stake', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 100n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 100n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), environmentConfig: { minStakePerSponsorship: 300n }, @@ -90,7 +90,7 @@ describe('payoutProportionalStrategy', () => { // unstakes must happen first because otherwise there isn't enough tokens for staking it('sends out unstakes before stakes', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ [ 'a', 30n ], [ 'b', 70n ], ]) }, @@ -113,7 +113,7 @@ describe('payoutProportionalStrategy', () => { it('unstakes from expired sponsorships', async () => { // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([[ 'b', 100n ]]) }, + operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([[ 'b', 100n ]]) }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -127,7 +127,7 @@ describe('payoutProportionalStrategy', () => { it('restakes expired sponsorship stakes into other sponsorships', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ [ 'a', 100n ], [ 'b', 100n ], [ 'c', 100n ], @@ -147,7 +147,7 @@ describe('payoutProportionalStrategy', () => { it('handles rounding errors', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -164,7 +164,7 @@ describe('payoutProportionalStrategy', () => { it('rounding error no-op case', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ ['a', 166n ], ['b', 166n ], ['c', 668n ], @@ -181,7 +181,7 @@ describe('payoutProportionalStrategy', () => { it('uses Infinity as default maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -195,7 +195,7 @@ describe('payoutProportionalStrategy', () => { it('handles greater than MAX_SAFE_INTEGER payout values correctly', () => { const stakes = adjustStakes({ - operatorState: { unstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 3n * BigInt(Number.MAX_SAFE_INTEGER) }], @@ -210,7 +210,7 @@ describe('payoutProportionalStrategy', () => { it('operators may choose different sponsorships if payoutPerSec are same', () => { const createArgs = (operatorContractAddress: string) => { return { - operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 0n, operatorContractAddress }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -231,7 +231,7 @@ describe('payoutProportionalStrategy', () => { describe('exclude small transactions', () => { it('exclude small stakings', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 1000n, myCurrentStakes: new Map() }, + operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -246,7 +246,7 @@ describe('payoutProportionalStrategy', () => { it('one small transaction is balanced by removing one staking', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 820n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 820n, myCurrentStakes: new Map([ ['a', 180n] ]) }, operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, @@ -263,7 +263,7 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing multiple stakings', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 740n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 740n, myCurrentStakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n] @@ -284,7 +284,7 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing all stakings', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 359n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 359n, myCurrentStakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n], @@ -304,7 +304,7 @@ describe('payoutProportionalStrategy', () => { it('very small expiration unstake and nothing to stake', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ ['a', 10n], ['b', 1000n] ]) }, @@ -319,7 +319,7 @@ describe('payoutProportionalStrategy', () => { it('only multiple very small expiration unstakes', () => { expect(adjustStakes({ - operatorState: { unstakedAmount: 0n, myCurrentStakes: new Map([ + operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ ['a', 10n], ['b', 20n] ]) }, From a2d4c3af43e47ca535e22f65174f950890c0b1a9 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 19:57:13 +0300 Subject: [PATCH 57/60] inline OperatorState interface --- .../plugins/autostaker/AutostakerPlugin.ts | 6 +- .../autostaker/payoutProportionalStrategy.ts | 17 ++-- packages/node/src/plugins/autostaker/types.ts | 10 +-- .../payoutProportionalStrategy.test.ts | 78 ++++++++++++------- 4 files changed, 63 insertions(+), 48 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 796000ccd8..8ef24d0e1a 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -104,10 +104,8 @@ export class AutostakerPlugin extends Plugin { } }) const actions = adjustStakes({ - operatorState: { - myCurrentStakes, - myUnstakedAmount - }, + myCurrentStakes, + myUnstakedAmount, operatorConfig: { operatorContractAddress: this.pluginConfig.operatorContractAddress, minTransactionAmount: parseEther(String(this.pluginConfig.minTransactionDataTokenAmount)), diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index dc4b030490..415083e27d 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -96,8 +96,8 @@ const getSelectedSponsorships = ( */ const getTargetStakes = ( myCurrentStakes: Map, - stakeableSponsorships: Map, myUnstakedAmount: WeiAmount, + stakeableSponsorships: Map, minStakePerSponsorship: WeiAmount, maxSponsorshipCount: number | undefined, operatorContractAddress: string @@ -130,16 +130,17 @@ const getTargetStakes = ( * e.g. all unstake actions first to ensure sufficient balance for the subsequent staking actions. */ export const adjustStakes: AdjustStakesFn = ({ - operatorState, + myCurrentStakes, + myUnstakedAmount, operatorConfig, stakeableSponsorships, environmentConfig }): Action[] => { const targetStakes = getTargetStakes( - operatorState.myCurrentStakes, + myCurrentStakes, + myUnstakedAmount, stakeableSponsorships, - operatorState.myUnstakedAmount, environmentConfig.minStakePerSponsorship, operatorConfig.maxSponsorshipCount, operatorConfig.operatorContractAddress @@ -148,15 +149,15 @@ export const adjustStakes: AdjustStakesFn = ({ const adjustments = [...targetStakes.keys()] .map((sponsorshipId) => ({ sponsorshipId, - difference: targetStakes.get(sponsorshipId)! - (operatorState.myCurrentStakes.get(sponsorshipId) ?? 0n) + difference: targetStakes.get(sponsorshipId)! - (myCurrentStakes.get(sponsorshipId) ?? 0n) })) .filter(({ difference: difference }) => difference !== 0n) // fix rounding errors by forcing the net staking to equal myUnstakedAmount: adjust the largest staking const netStakingAmount = sum(adjustments.map((a) => a.difference)) - if (netStakingAmount !== operatorState.myUnstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { + if (netStakingAmount !== myUnstakedAmount && stakeableSponsorships.size > 0 && adjustments.length > 0) { const largestDifference = maxBy(adjustments, (a) => a.difference)! - largestDifference.difference += operatorState.myUnstakedAmount - netStakingAmount + largestDifference.difference += myUnstakedAmount - netStakingAmount if (largestDifference.difference === 0n) { pull(adjustments, largestDifference) } @@ -172,7 +173,7 @@ export const adjustStakes: AdjustStakesFn = ({ const stakings = adjustments.filter((a) => a.difference > 0) const unstakings = adjustments.filter((a) => a.difference < 0) const stakingSum = sum(stakings.map((a) => a.difference)) - const availableSum = abs(sum(unstakings.map((a) => a.difference))) + operatorState.myUnstakedAmount + const availableSum = abs(sum(unstakings.map((a) => a.difference))) + myUnstakedAmount if (stakingSum > availableSum) { const smallestStaking = minBy(stakings, (a) => a.difference)! pull(adjustments, smallestStaking) diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 606fdb9e89..437d75dfea 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -12,9 +12,10 @@ export interface Action { } export type AdjustStakesFn = (opts: { - operatorState: OperatorState - operatorConfig: OperatorConfig + myCurrentStakes: Map + myUnstakedAmount: WeiAmount stakeableSponsorships: Map + operatorConfig: OperatorConfig environmentConfig: EnvironmentConfig }) => Action[] @@ -22,11 +23,6 @@ export interface SponsorshipConfig { payoutPerSec: WeiAmount } -export interface OperatorState { - myCurrentStakes: Map - myUnstakedAmount: WeiAmount -} - export interface OperatorConfig { operatorContractAddress: string minTransactionAmount: WeiAmount diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index af82187830..2e7ff443e4 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -5,7 +5,8 @@ describe('payoutProportionalStrategy', () => { it('stake all', () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 11000n, myCurrentStakes: new Map() }, + myUnstakedAmount: 11000n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 2n }], @@ -16,12 +17,13 @@ describe('payoutProportionalStrategy', () => { })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'b', amount: 5400n }, { type: 'stake', sponsorshipId: 'c', amount: 5600n } - ]) + ]) }) it('unstakes everything if no stakeable sponsorships', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map([[ 'a', 2000n ]]) }, + myUnstakedAmount: 1000n, + myCurrentStakes: new Map([[ 'a', 2000n ]]), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), environmentConfig: { minStakePerSponsorship: 1234n }, @@ -32,7 +34,8 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to stakeableSponsorships.size', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 600n, myCurrentStakes: new Map() }, + myUnstakedAmount: 600n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -49,7 +52,8 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 500n, myCurrentStakes: new Map() }, + myUnstakedAmount: 500n, + myCurrentStakes: new Map(), operatorConfig: { maxSponsorshipCount: 2, minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -65,7 +69,8 @@ describe('payoutProportionalStrategy', () => { it('limits the targetSponsorshipCount to minStakePerSponsorship and available tokens', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 500n, myCurrentStakes: new Map() }, + myUnstakedAmount: 500n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included @@ -80,7 +85,8 @@ describe('payoutProportionalStrategy', () => { it('doesn\'t allocate tokens if less available than minimum stake', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 100n, myCurrentStakes: new Map() }, + myUnstakedAmount: 100n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), environmentConfig: { minStakePerSponsorship: 300n }, @@ -90,10 +96,11 @@ describe('payoutProportionalStrategy', () => { // unstakes must happen first because otherwise there isn't enough tokens for staking it('sends out unstakes before stakes', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ + myUnstakedAmount: 0n, + myCurrentStakes: new Map([ [ 'a', 30n ], [ 'b', 70n ], - ]) }, + ]), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 40n }], // add stake here @@ -113,7 +120,8 @@ describe('payoutProportionalStrategy', () => { it('unstakes from expired sponsorships', async () => { // currently staked into b, but b has expired, so it's not included in the stakeableSponsorships expect(adjustStakes({ - operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([[ 'b', 100n ]]) }, + myUnstakedAmount: 0n, + myCurrentStakes: new Map([[ 'b', 100n ]]), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -127,11 +135,12 @@ describe('payoutProportionalStrategy', () => { it('restakes expired sponsorship stakes into other sponsorships', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ + myUnstakedAmount: 0n, + myCurrentStakes: new Map([ [ 'a', 100n ], [ 'b', 100n ], [ 'c', 100n ], - ]) }, + ]), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -147,7 +156,8 @@ describe('payoutProportionalStrategy', () => { it('handles rounding errors', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, + myUnstakedAmount: 1000n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -164,11 +174,12 @@ describe('payoutProportionalStrategy', () => { it('rounding error no-op case', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ + myUnstakedAmount: 0n, + myCurrentStakes: new Map([ ['a', 166n ], ['b', 166n ], ['c', 668n ], - ]) }, + ]), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -181,7 +192,8 @@ describe('payoutProportionalStrategy', () => { it('uses Infinity as default maxSponsorshipCount', async () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, + myUnstakedAmount: 1000n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -195,7 +207,8 @@ describe('payoutProportionalStrategy', () => { it('handles greater than MAX_SAFE_INTEGER payout values correctly', () => { const stakes = adjustStakes({ - operatorState: { myUnstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), myCurrentStakes: new Map() }, + myUnstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 3n * BigInt(Number.MAX_SAFE_INTEGER) }], @@ -210,7 +223,8 @@ describe('payoutProportionalStrategy', () => { it('operators may choose different sponsorships if payoutPerSec are same', () => { const createArgs = (operatorContractAddress: string) => { return { - operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, + myUnstakedAmount: 1000n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 0n, operatorContractAddress }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -231,7 +245,8 @@ describe('payoutProportionalStrategy', () => { describe('exclude small transactions', () => { it('exclude small stakings', () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 1000n, myCurrentStakes: new Map() }, + myUnstakedAmount: 1000n, + myCurrentStakes: new Map(), operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], @@ -246,9 +261,10 @@ describe('payoutProportionalStrategy', () => { it('one small transaction is balanced by removing one staking', () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 820n, myCurrentStakes: new Map([ + myUnstakedAmount: 820n, + myCurrentStakes: new Map([ ['a', 180n] - ]) }, + ]), operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -263,11 +279,12 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing multiple stakings', () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 740n, myCurrentStakes: new Map([ + myUnstakedAmount: 740n, + myCurrentStakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n] - ]) }, + ]), operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -284,12 +301,13 @@ describe('payoutProportionalStrategy', () => { it('multiple small transactions are balanced with by removing all stakings', () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 359n, myCurrentStakes: new Map([ + myUnstakedAmount: 359n, + myCurrentStakes: new Map([ ['a', 180n], ['b', 200n], ['c', 295n], ['e', 381n] - ]) }, + ]), operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], @@ -304,10 +322,11 @@ describe('payoutProportionalStrategy', () => { it('very small expiration unstake and nothing to stake', () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ + myUnstakedAmount: 0n, + myCurrentStakes: new Map([ ['a', 10n], ['b', 1000n] - ]) }, + ]), operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([]), environmentConfig: { minStakePerSponsorship: 0n } @@ -319,10 +338,11 @@ describe('payoutProportionalStrategy', () => { it('only multiple very small expiration unstakes', () => { expect(adjustStakes({ - operatorState: { myUnstakedAmount: 0n, myCurrentStakes: new Map([ + myUnstakedAmount: 0n, + myCurrentStakes: new Map([ ['a', 10n], ['b', 20n] - ]) }, + ]), operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([]), environmentConfig: { minStakePerSponsorship: 0n } From 3726cffba1650ad90dde3b39791435792d961848 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 20:16:24 +0300 Subject: [PATCH 58/60] inline OperatorConfig and EnvironmentConfig interfaces --- .../plugins/autostaker/AutostakerPlugin.ts | 12 +-- .../autostaker/payoutProportionalStrategy.ts | 26 ++--- packages/node/src/plugins/autostaker/types.ts | 16 +-- .../payoutProportionalStrategy.test.ts | 101 +++++++++++------- 4 files changed, 83 insertions(+), 72 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 8ef24d0e1a..deb0555600 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -106,15 +106,11 @@ export class AutostakerPlugin extends Plugin { const actions = adjustStakes({ myCurrentStakes, myUnstakedAmount, - operatorConfig: { - operatorContractAddress: this.pluginConfig.operatorContractAddress, - minTransactionAmount: parseEther(String(this.pluginConfig.minTransactionDataTokenAmount)), - maxSponsorshipCount: this.pluginConfig.maxSponsorshipCount - }, stakeableSponsorships, - environmentConfig: { - minStakePerSponsorship - } + operatorContractAddress: this.pluginConfig.operatorContractAddress, + maxSponsorshipCount: this.pluginConfig.maxSponsorshipCount, + minTransactionAmount: parseEther(String(this.pluginConfig.minTransactionDataTokenAmount)), + minStakePerSponsorship }) const signer = await streamrClient.getSigner() for (const action of actions) { diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index 415083e27d..db01188b33 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -60,9 +60,9 @@ const getSelectedSponsorships = ( myCurrentStakes: Map, stakeableSponsorships: Map, totalStakeableAmount: WeiAmount, - minStakePerSponsorship: WeiAmount, + operatorContractAddress: string, maxSponsorshipCount: number | undefined, - operatorContractAddress: string + minStakePerSponsorship: WeiAmount ): SponsorshipID[] => { const count = Math.min( stakeableSponsorships.size, @@ -98,18 +98,18 @@ const getTargetStakes = ( myCurrentStakes: Map, myUnstakedAmount: WeiAmount, stakeableSponsorships: Map, - minStakePerSponsorship: WeiAmount, + operatorContractAddress: string, maxSponsorshipCount: number | undefined, - operatorContractAddress: string + minStakePerSponsorship: WeiAmount ): Map => { const totalStakeableAmount = sum([...myCurrentStakes.values()]) + myUnstakedAmount const selectedSponsorships = getSelectedSponsorships( myCurrentStakes, stakeableSponsorships, totalStakeableAmount, - minStakePerSponsorship, + operatorContractAddress, maxSponsorshipCount, - operatorContractAddress + minStakePerSponsorship ) const minStakePerSponsorshipSum = BigInt(selectedSponsorships.length) * minStakePerSponsorship const payoutProportionalAmount = totalStakeableAmount - minStakePerSponsorshipSum @@ -132,18 +132,20 @@ const getTargetStakes = ( export const adjustStakes: AdjustStakesFn = ({ myCurrentStakes, myUnstakedAmount, - operatorConfig, stakeableSponsorships, - environmentConfig + operatorContractAddress, + maxSponsorshipCount, + minTransactionAmount, + minStakePerSponsorship }): Action[] => { const targetStakes = getTargetStakes( myCurrentStakes, myUnstakedAmount, stakeableSponsorships, - environmentConfig.minStakePerSponsorship, - operatorConfig.maxSponsorshipCount, - operatorConfig.operatorContractAddress + operatorContractAddress, + maxSponsorshipCount, + minStakePerSponsorship ) const adjustments = [...targetStakes.keys()] @@ -165,7 +167,7 @@ export const adjustStakes: AdjustStakesFn = ({ const tooSmallAdjustments = adjustments.filter( // note the edge case: expired sponsorships can be unstaked, even if the transaction amount is considered "too small" - (a) => (abs(a.difference) < operatorConfig.minTransactionAmount) && stakeableSponsorships.has(a.sponsorshipId) + (a) => (abs(a.difference) < minTransactionAmount) && stakeableSponsorships.has(a.sponsorshipId) ) if (tooSmallAdjustments.length > 0) { pull(adjustments, ...tooSmallAdjustments) diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index 437d75dfea..af0322eba5 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -15,21 +15,13 @@ export type AdjustStakesFn = (opts: { myCurrentStakes: Map myUnstakedAmount: WeiAmount stakeableSponsorships: Map - operatorConfig: OperatorConfig - environmentConfig: EnvironmentConfig + operatorContractAddress: string + maxSponsorshipCount?: number + minTransactionAmount: WeiAmount + minStakePerSponsorship: WeiAmount }) => Action[] export interface SponsorshipConfig { payoutPerSec: WeiAmount } -export interface OperatorConfig { - operatorContractAddress: string - minTransactionAmount: WeiAmount - maxSponsorshipCount?: number -} - -export interface EnvironmentConfig { - minStakePerSponsorship: WeiAmount -} - diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 2e7ff443e4..0a209e2dde 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -7,13 +7,14 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 11000n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 2n }], ['b', { payoutPerSec: 4n }], ['c', { payoutPerSec: 6n }], ]), - environmentConfig: { minStakePerSponsorship: 5000n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 5000n })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'b', amount: 5400n }, { type: 'stake', sponsorshipId: 'c', amount: 5600n } @@ -24,9 +25,10 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 1000n, myCurrentStakes: new Map([[ 'a', 2000n ]]), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map(), - environmentConfig: { minStakePerSponsorship: 1234n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 1234n })).toEqual([ { type: 'unstake', sponsorshipId: 'a', amount: 2000n }, ]) @@ -36,13 +38,14 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 600n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 20n }], ['c', { payoutPerSec: 30n }], ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ { type: 'stake', sponsorshipId: 'a', amount: 100n }, { type: 'stake', sponsorshipId: 'b', amount: 200n }, @@ -54,13 +57,15 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 500n, myCurrentStakes: new Map(), - operatorConfig: { maxSponsorshipCount: 2, minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included ['b', { payoutPerSec: 20n }], // included ['c', { payoutPerSec: 30n }], // included ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + maxSponsorshipCount: 2, + minTransactionAmount: 0n, + minStakePerSponsorship: 0n }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ { type: 'stake', sponsorshipId: 'b', amount: 200n }, { type: 'stake', sponsorshipId: 'c', amount: 300n }, @@ -71,13 +76,14 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 500n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], // not included ['b', { payoutPerSec: 20n }], // not included ['c', { payoutPerSec: 30n }], // included ]), - environmentConfig: { minStakePerSponsorship: 300n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 300n }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ { type: 'stake', sponsorshipId: 'c', amount: 500n }, ]) @@ -87,9 +93,10 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 100n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), - environmentConfig: { minStakePerSponsorship: 300n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 300n })).toEqual([]) }) @@ -101,14 +108,15 @@ describe('payoutProportionalStrategy', () => { [ 'a', 30n ], [ 'b', 70n ], ]), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 40n }], // add stake here ['b', { payoutPerSec: 30n }], // unstake from here ['c', { payoutPerSec: 20n }], // stake here ['d', { payoutPerSec: 10n }], // stake here ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n }).map((a) => a.type)).toEqual([ 'unstake', 'stake', @@ -122,11 +130,12 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 0n, myCurrentStakes: new Map([[ 'b', 100n ]]), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'b', amount: 100n }, { type: 'stake', sponsorshipId: 'a', amount: 100n }, @@ -141,12 +150,13 @@ describe('payoutProportionalStrategy', () => { [ 'b', 100n ], [ 'c', 100n ], ]), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 10n }], ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'c', amount: 100n }, { type: 'stake', sponsorshipId: 'a', amount: 50n }, @@ -158,13 +168,14 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 1000n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], ['c', { payoutPerSec: 400n }], ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'a', amount: 166n }, { type: 'stake', sponsorshipId: 'b', amount: 166n }, @@ -180,13 +191,14 @@ describe('payoutProportionalStrategy', () => { ['b', 166n ], ['c', 668n ], ]), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], ['c', { payoutPerSec: 400n }], ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n })).toEqual([]) }) @@ -194,14 +206,15 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 1000n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 20n }], ['c', { payoutPerSec: 30n }], ['d', { payoutPerSec: 40n }], ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n })).toHaveLength(4) }) @@ -209,13 +222,14 @@ describe('payoutProportionalStrategy', () => { const stakes = adjustStakes({ myUnstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 3n * BigInt(Number.MAX_SAFE_INTEGER) }], ['b', { payoutPerSec: 4n * BigInt(Number.MAX_SAFE_INTEGER) }], ['c', { payoutPerSec: 2n * BigInt(Number.MAX_SAFE_INTEGER) }], ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 0n, + minStakePerSponsorship: 0n }) expect(sortBy(stakes, (a) => a.amount).map((a) => a.sponsorshipId)).toEqual(['c', 'a', 'b']) }) @@ -225,12 +239,13 @@ describe('payoutProportionalStrategy', () => { return { myUnstakedAmount: 1000n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 0n, operatorContractAddress }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], ]), - environmentConfig: { minStakePerSponsorship: 1000n }, + operatorContractAddress, + minTransactionAmount: 0n, + minStakePerSponsorship: 1000n } } const stakesForOperator1 = adjustStakes(createArgs('0x1111')) @@ -247,13 +262,14 @@ describe('payoutProportionalStrategy', () => { expect(adjustStakes({ myUnstakedAmount: 1000n, myCurrentStakes: new Map(), - operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 10n }], ['b', { payoutPerSec: 20n }], ['c', { payoutPerSec: 1000n }] ]), - environmentConfig: { minStakePerSponsorship: 0n }, + operatorContractAddress: '', + minTransactionAmount: 20n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'c', amount: 972n } ]) @@ -265,13 +281,14 @@ describe('payoutProportionalStrategy', () => { myCurrentStakes: new Map([ ['a', 180n] ]), - operatorConfig: { minTransactionAmount: 20n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], ['c', { payoutPerSec: 400n }], ]), - environmentConfig: { minStakePerSponsorship: 0n } + operatorContractAddress: '', + minTransactionAmount: 20n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'c', amount: 668n } ]) @@ -285,7 +302,6 @@ describe('payoutProportionalStrategy', () => { ['b', 200n], ['c', 295n] ]), - operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], @@ -293,7 +309,9 @@ describe('payoutProportionalStrategy', () => { ['d', { payoutPerSec: 220n }], ['e', { payoutPerSec: 230n }] ]), - environmentConfig: { minStakePerSponsorship: 0n } + operatorContractAddress: '', + minTransactionAmount: 50n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'stake', sponsorshipId: 'e', amount: 381n } ]) @@ -308,7 +326,6 @@ describe('payoutProportionalStrategy', () => { ['c', 295n], ['e', 381n] ]), - operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([ ['a', { payoutPerSec: 100n }], ['b', { payoutPerSec: 100n }], @@ -316,7 +333,9 @@ describe('payoutProportionalStrategy', () => { ['d', { payoutPerSec: 220n }], ['e', { payoutPerSec: 230n }] ]), - environmentConfig: { minStakePerSponsorship: 0n } + operatorContractAddress: '', + minTransactionAmount: 50n, + minStakePerSponsorship: 0n })).toEqual([]) }) @@ -327,9 +346,10 @@ describe('payoutProportionalStrategy', () => { ['a', 10n], ['b', 1000n] ]), - operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([]), - environmentConfig: { minStakePerSponsorship: 0n } + operatorContractAddress: '', + minTransactionAmount: 50n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'a', amount: 10n }, { type: 'unstake', sponsorshipId: 'b', amount: 1000n } @@ -343,9 +363,10 @@ describe('payoutProportionalStrategy', () => { ['a', 10n], ['b', 20n] ]), - operatorConfig: { minTransactionAmount: 50n, operatorContractAddress: '' }, stakeableSponsorships: new Map([]), - environmentConfig: { minStakePerSponsorship: 0n } + operatorContractAddress: '', + minTransactionAmount: 50n, + minStakePerSponsorship: 0n })).toIncludeSameMembers([ { type: 'unstake', sponsorshipId: 'a', amount: 10n }, { type: 'unstake', sponsorshipId: 'b', amount: 20n } From d0353de530168a3379cc8e37ff002036bcb9bd5e Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Thu, 5 Jun 2025 20:19:21 +0300 Subject: [PATCH 59/60] AutostakerPluginConfig fields in same order as in adjustStakes() --- .../src/plugins/autostaker/AutostakerPlugin.ts | 4 ++-- .../node/src/plugins/autostaker/config.schema.json | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index deb0555600..1a99456641 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -9,10 +9,10 @@ import { Action, SponsorshipConfig, SponsorshipID } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string - runIntervalInMs: number + maxSponsorshipCount?: number minTransactionDataTokenAmount: number maxAcceptableMinOperatorCount: number - maxSponsorshipCount?: number + runIntervalInMs: number } interface SponsorshipQueryResultItem { diff --git a/packages/node/src/plugins/autostaker/config.schema.json b/packages/node/src/plugins/autostaker/config.schema.json index 060abb9d0d..18bfb27863 100644 --- a/packages/node/src/plugins/autostaker/config.schema.json +++ b/packages/node/src/plugins/autostaker/config.schema.json @@ -13,11 +13,10 @@ "description": "Operator contract Ethereum address", "format": "ethereum-address" }, - "runIntervalInMs": { + "maxSponsorshipCount": { "type": "integer", - "description": "The interval (in milliseconds) at which autostaking possibilities are analyzed and executed", - "minimum": 0, - "default": 30000 + "description": "Maximum count of sponsorships which are staked at any given time", + "minimum": 1 }, "minTransactionDataTokenAmount": { "type": "integer", @@ -31,10 +30,11 @@ "minimum": 0, "default": 100 }, - "maxSponsorshipCount": { + "runIntervalInMs": { "type": "integer", - "description": "Maximum count of sponsorships which are staked at any given time", - "minimum": 1 + "description": "The interval (in milliseconds) at which autostaking possibilities are analyzed and executed", + "minimum": 0, + "default": 30000 } } } From 49e6d96031a5bc32b5c5ff407d3cfea684acfaea Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Fri, 6 Jun 2025 16:53:42 +0300 Subject: [PATCH 60/60] config defaults --- .../plugins/autostaker/AutostakerPlugin.ts | 2 +- .../src/plugins/autostaker/config.schema.json | 5 +-- .../autostaker/payoutProportionalStrategy.ts | 6 ++-- packages/node/src/plugins/autostaker/types.ts | 2 +- .../payoutProportionalStrategy.test.ts | 34 ++++++++++--------- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts index 1a99456641..d332e91276 100644 --- a/packages/node/src/plugins/autostaker/AutostakerPlugin.ts +++ b/packages/node/src/plugins/autostaker/AutostakerPlugin.ts @@ -9,7 +9,7 @@ import { Action, SponsorshipConfig, SponsorshipID } from './types' export interface AutostakerPluginConfig { operatorContractAddress: string - maxSponsorshipCount?: number + maxSponsorshipCount: number minTransactionDataTokenAmount: number maxAcceptableMinOperatorCount: number runIntervalInMs: number diff --git a/packages/node/src/plugins/autostaker/config.schema.json b/packages/node/src/plugins/autostaker/config.schema.json index 18bfb27863..cfbda633bd 100644 --- a/packages/node/src/plugins/autostaker/config.schema.json +++ b/packages/node/src/plugins/autostaker/config.schema.json @@ -16,7 +16,8 @@ "maxSponsorshipCount": { "type": "integer", "description": "Maximum count of sponsorships which are staked at any given time", - "minimum": 1 + "minimum": 1, + "default": 100 }, "minTransactionDataTokenAmount": { "type": "integer", @@ -34,7 +35,7 @@ "type": "integer", "description": "The interval (in milliseconds) at which autostaking possibilities are analyzed and executed", "minimum": 0, - "default": 30000 + "default": 3600000 } } } diff --git a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts index db01188b33..d4979261d2 100644 --- a/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts +++ b/packages/node/src/plugins/autostaker/payoutProportionalStrategy.ts @@ -61,12 +61,12 @@ const getSelectedSponsorships = ( stakeableSponsorships: Map, totalStakeableAmount: WeiAmount, operatorContractAddress: string, - maxSponsorshipCount: number | undefined, + maxSponsorshipCount: number, minStakePerSponsorship: WeiAmount ): SponsorshipID[] => { const count = Math.min( stakeableSponsorships.size, - maxSponsorshipCount ?? Infinity, + maxSponsorshipCount, (minStakePerSponsorship > 0n) ? Number(totalStakeableAmount / minStakePerSponsorship) : Infinity // as many as we can afford ) const [ @@ -99,7 +99,7 @@ const getTargetStakes = ( myUnstakedAmount: WeiAmount, stakeableSponsorships: Map, operatorContractAddress: string, - maxSponsorshipCount: number | undefined, + maxSponsorshipCount: number, minStakePerSponsorship: WeiAmount ): Map => { const totalStakeableAmount = sum([...myCurrentStakes.values()]) + myUnstakedAmount diff --git a/packages/node/src/plugins/autostaker/types.ts b/packages/node/src/plugins/autostaker/types.ts index af0322eba5..2aa031e455 100644 --- a/packages/node/src/plugins/autostaker/types.ts +++ b/packages/node/src/plugins/autostaker/types.ts @@ -16,7 +16,7 @@ export type AdjustStakesFn = (opts: { myUnstakedAmount: WeiAmount stakeableSponsorships: Map operatorContractAddress: string - maxSponsorshipCount?: number + maxSponsorshipCount: number minTransactionAmount: WeiAmount minStakePerSponsorship: WeiAmount }) => Action[] diff --git a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts index 0a209e2dde..fecbae6fd9 100644 --- a/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts +++ b/packages/node/test/unit/plugins/autostaker/payoutProportionalStrategy.test.ts @@ -13,6 +13,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 6n }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 5000n })).toIncludeSameMembers([ @@ -27,6 +28,7 @@ describe('payoutProportionalStrategy', () => { myCurrentStakes: new Map([[ 'a', 2000n ]]), stakeableSponsorships: new Map(), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 1234n })).toEqual([ @@ -44,6 +46,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 30n }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 0n }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -82,6 +85,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 30n }], // included ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 300n }).sort((a, b) => a.sponsorshipId.localeCompare(b.sponsorshipId))).toEqual([ @@ -95,6 +99,7 @@ describe('payoutProportionalStrategy', () => { myCurrentStakes: new Map(), stakeableSponsorships: new Map([['a', { payoutPerSec: 10n }]]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 300n })).toEqual([]) @@ -115,6 +120,7 @@ describe('payoutProportionalStrategy', () => { ['d', { payoutPerSec: 10n }], // stake here ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 0n }).map((a) => a.type)).toEqual([ @@ -134,6 +140,7 @@ describe('payoutProportionalStrategy', () => { ['a', { payoutPerSec: 10n }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 0n })).toIncludeSameMembers([ @@ -155,6 +162,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 10n }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 0n })).toIncludeSameMembers([ @@ -174,6 +182,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 400n }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 0n })).toIncludeSameMembers([ @@ -197,27 +206,12 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 400n }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 0n })).toEqual([]) }) - it('uses Infinity as default maxSponsorshipCount', async () => { - expect(adjustStakes({ - myUnstakedAmount: 1000n, - myCurrentStakes: new Map(), - stakeableSponsorships: new Map([ - ['a', { payoutPerSec: 10n }], - ['b', { payoutPerSec: 20n }], - ['c', { payoutPerSec: 30n }], - ['d', { payoutPerSec: 40n }], - ]), - operatorContractAddress: '', - minTransactionAmount: 0n, - minStakePerSponsorship: 0n - })).toHaveLength(4) - }) - it('handles greater than MAX_SAFE_INTEGER payout values correctly', () => { const stakes = adjustStakes({ myUnstakedAmount: 9n * BigInt(Number.MAX_SAFE_INTEGER), @@ -228,6 +222,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 2n * BigInt(Number.MAX_SAFE_INTEGER) }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 0n }) @@ -244,6 +239,7 @@ describe('payoutProportionalStrategy', () => { ['b', { payoutPerSec: 100n }], ]), operatorContractAddress, + maxSponsorshipCount: 100, minTransactionAmount: 0n, minStakePerSponsorship: 1000n } @@ -268,6 +264,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 1000n }] ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 20n, minStakePerSponsorship: 0n })).toIncludeSameMembers([ @@ -287,6 +284,7 @@ describe('payoutProportionalStrategy', () => { ['c', { payoutPerSec: 400n }], ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 20n, minStakePerSponsorship: 0n })).toIncludeSameMembers([ @@ -310,6 +308,7 @@ describe('payoutProportionalStrategy', () => { ['e', { payoutPerSec: 230n }] ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 50n, minStakePerSponsorship: 0n })).toIncludeSameMembers([ @@ -334,6 +333,7 @@ describe('payoutProportionalStrategy', () => { ['e', { payoutPerSec: 230n }] ]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 50n, minStakePerSponsorship: 0n })).toEqual([]) @@ -348,6 +348,7 @@ describe('payoutProportionalStrategy', () => { ]), stakeableSponsorships: new Map([]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 50n, minStakePerSponsorship: 0n })).toIncludeSameMembers([ @@ -365,6 +366,7 @@ describe('payoutProportionalStrategy', () => { ]), stakeableSponsorships: new Map([]), operatorContractAddress: '', + maxSponsorshipCount: 100, minTransactionAmount: 50n, minStakePerSponsorship: 0n })).toIncludeSameMembers([