Skip to content

feat(node): [NET-1455] Autostaker plugin #3086

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 64 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
6f23cee
first draft
teogeb Apr 25, 2025
252bd94
Merge branch 'main' into autostaker-plugin
teogeb May 15, 2025
d4968e5
payoutProportional strategy (mostly copy-paste from autostaker repo)
teogeb May 19, 2025
bd0d77b
manual test: mulltiple sponsorships
teogeb May 19, 2025
b6d76cf
use StreamrClient#getTheGraphClient()
teogeb May 28, 2025
a616556
cli-tools command
teogeb May 28, 2025
3e8bfcf
grant controller role to the node
teogeb May 28, 2025
49f673d
rm guard
teogeb Jun 2, 2025
f2a44c3
improve test
teogeb Jun 2, 2025
c08fc53
simpler comparison
teogeb Jun 2, 2025
4f6488f
refactor: getTargetStakes()
teogeb Jun 2, 2025
4159ba2
rm obsolete totalStakedWei field
teogeb Jun 2, 2025
6fcc2e9
rename SponsorshipId
teogeb Jun 2, 2025
d8a2156
use WeiAmount type
teogeb Jun 2, 2025
19a8134
rename SponsorshipState
teogeb Jun 2, 2025
8fc1e3e
tests
teogeb Jun 2, 2025
cf639ab
getSelectedSponsorships()
teogeb Jun 2, 2025
a7e262b
getTargetStakes() includes expired sponsorships
teogeb Jun 2, 2025
d3cd2dc
getTargetStakes() calls getSelectedSponsorships()
teogeb Jun 2, 2025
17c1a04
sort only by adjustment type
teogeb Jun 2, 2025
2b7b482
rename variable
teogeb Jun 2, 2025
6d583bb
filter out small transactions
teogeb Jun 2, 2025
263605f
rm comment
teogeb Jun 2, 2025
e3e05eb
scheduleAtApproximateInterval()
teogeb Jun 2, 2025
cc531fe
use scheduleAtApproximateInterval()
teogeb Jun 2, 2025
81dda13
select sponsorships based on operatorContractAddress if totalPayoutWe…
teogeb Jun 2, 2025
edcb598
smoke test
teogeb Jun 2, 2025
b622b47
eslint
teogeb Jun 2, 2025
9fe13e5
maxSponsorshipCount in plugin config
teogeb Jun 2, 2025
cfce154
minTransactionDataTokenAmount in plugin config
teogeb Jun 2, 2025
4b9f5a2
rename totalPayoutWeiPerSec -> payoutPerSec
teogeb Jun 2, 2025
4c3c0d7
rename unstakedWei -> unstakedAmount
teogeb Jun 2, 2025
e19898f
rename minTransactionWei -> minTransactionAmount
teogeb Jun 2, 2025
82c6177
rename minimumStakeWei -> minStakePerSponsorship
teogeb Jun 2, 2025
3b71b7b
rename local variables
teogeb Jun 2, 2025
4da3320
logging
teogeb Jun 2, 2025
6f0e77a
reduce logging in TheGraphClient
teogeb Jun 2, 2025
6cab147
rm manual test
teogeb Jun 2, 2025
b11a765
Merge branch 'main' into autostaker-plugin
teogeb Jun 2, 2025
82dfd25
rm ethereum-private-key addition (not needed as we don't configure pr…
teogeb Jun 2, 2025
03a3077
rm unnecessary filter in getStakeableSponsorships()
teogeb Jun 2, 2025
0a59667
Merge branch 'main' into autostaker-plugin
teogeb Jun 2, 2025
0a43315
improve logging
teogeb Jun 3, 2025
2e7fa15
fetchMinStakePerSponsorship()
teogeb Jun 3, 2025
95facd5
style
teogeb Jun 3, 2025
84775ae
move sum() to payoutProportionalStrategy.ts
teogeb Jun 3, 2025
70cf31d
minimumStakingPeriodSeconds
teogeb Jun 4, 2025
44028c2
maxAcceptableMinOperatorCount
teogeb Jun 4, 2025
6b53523
maxOperators
teogeb Jun 4, 2025
b85a3d2
MIN_SPONSORSHIP_TOTAL_PAYOUT_PER_SECOND
teogeb Jun 5, 2025
9a84781
improve tooSmallAdjustments removal
teogeb Jun 5, 2025
5ddf14f
rm unnecessary variable
teogeb Jun 5, 2025
3ddfc92
add comment
teogeb Jun 5, 2025
a1c87b6
rm unnecessary Number -> bigint type conversions
teogeb Jun 5, 2025
9649cba
fix hasAcceptableOperatorCount() for sponsorships which we've already…
teogeb Jun 5, 2025
7caa8f0
always unstake from expired sponsorship
teogeb Jun 5, 2025
645a1ab
changelog
teogeb Jun 5, 2025
29cd6ce
rename stakes -> myCurrentStakes
teogeb Jun 5, 2025
a51bbc7
rename unstakedAmount -> myUnstakedAmount
teogeb Jun 5, 2025
a2d4c3a
inline OperatorState interface
teogeb Jun 5, 2025
3726cff
inline OperatorConfig and EnvironmentConfig interfaces
teogeb Jun 5, 2025
d0353de
AutostakerPluginConfig fields in same order as in adjustStakes()
teogeb Jun 5, 2025
49e6d96
config defaults
teogeb Jun 6, 2025
eec128f
Merge branch 'main' into autostaker-plugin
teogeb Jun 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/node/bin/autostaker-manual-test.sh
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions packages/node/src/config/validateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ': ') : ''
Expand Down
3 changes: 3 additions & 0 deletions packages/node/src/pluginRegistry.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -27,6 +28,8 @@ export const createPlugin = (name: string, brokerConfig: StrictConfig): Plugin<a
return new SubscriberPlugin(name, brokerConfig)
case 'info':
return new InfoPlugin(name, brokerConfig)
case 'autostaker':
return new AutostakerPlugin(name, brokerConfig)
default:
throw new Error(`Unknown plugin: ${name}`)
}
Expand Down
87 changes: 87 additions & 0 deletions packages/node/src/plugins/autostaker/AutostakerPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { _operatorContractUtils, 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 { Plugin } from '../../Plugin'
import PLUGIN_CONFIG_SCHEMA from './config.schema.json'

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
}

const STAKE_AMOUNT: WeiAmount = parseEther('10000')

const logger = new Logger(module)

export class AutostakerPlugin extends Plugin<AutostakerPluginConfig> {

private abortController: AbortController = new AbortController()

async start(streamrClient: StreamrClient): Promise<void> {
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<void> {
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<void> {
logger.info('Stop autostaker plugin')
this.abortController.abort()
}

// eslint-disable-next-line class-methods-use-this
override getConfigSchema(): Schema {
return PLUGIN_CONFIG_SCHEMA
}
}
29 changes: 29 additions & 0 deletions packages/node/src/plugins/autostaker/config.schema.json
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be maybe every 30min or smth?

}
}
}