-
Notifications
You must be signed in to change notification settings - Fork 43
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
Changes from all commits
Commits
Show all changes
64 commits
Select commit
Hold shift + click to select a range
6f23cee
first draft
teogeb 252bd94
Merge branch 'main' into autostaker-plugin
teogeb d4968e5
payoutProportional strategy (mostly copy-paste from autostaker repo)
teogeb bd0d77b
manual test: mulltiple sponsorships
teogeb b6d76cf
use StreamrClient#getTheGraphClient()
teogeb a616556
cli-tools command
teogeb 3e8bfcf
grant controller role to the node
teogeb 49f673d
rm guard
teogeb f2a44c3
improve test
teogeb c08fc53
simpler comparison
teogeb 4f6488f
refactor: getTargetStakes()
teogeb 4159ba2
rm obsolete totalStakedWei field
teogeb 6fcc2e9
rename SponsorshipId
teogeb d8a2156
use WeiAmount type
teogeb 19a8134
rename SponsorshipState
teogeb 8fc1e3e
tests
teogeb cf639ab
getSelectedSponsorships()
teogeb a7e262b
getTargetStakes() includes expired sponsorships
teogeb d3cd2dc
getTargetStakes() calls getSelectedSponsorships()
teogeb 17c1a04
sort only by adjustment type
teogeb 2b7b482
rename variable
teogeb 6d583bb
filter out small transactions
teogeb 263605f
rm comment
teogeb e3e05eb
scheduleAtApproximateInterval()
teogeb cc531fe
use scheduleAtApproximateInterval()
teogeb 81dda13
select sponsorships based on operatorContractAddress if totalPayoutWe…
teogeb edcb598
smoke test
teogeb b622b47
eslint
teogeb 9fe13e5
maxSponsorshipCount in plugin config
teogeb cfce154
minTransactionDataTokenAmount in plugin config
teogeb 4b9f5a2
rename totalPayoutWeiPerSec -> payoutPerSec
teogeb 4c3c0d7
rename unstakedWei -> unstakedAmount
teogeb e19898f
rename minTransactionWei -> minTransactionAmount
teogeb 82c6177
rename minimumStakeWei -> minStakePerSponsorship
teogeb 3b71b7b
rename local variables
teogeb 4da3320
logging
teogeb 6f0e77a
reduce logging in TheGraphClient
teogeb 6cab147
rm manual test
teogeb b11a765
Merge branch 'main' into autostaker-plugin
teogeb 82dfd25
rm ethereum-private-key addition (not needed as we don't configure pr…
teogeb 03a3077
rm unnecessary filter in getStakeableSponsorships()
teogeb 0a59667
Merge branch 'main' into autostaker-plugin
teogeb 0a43315
improve logging
teogeb 2e7fa15
fetchMinStakePerSponsorship()
teogeb 95facd5
style
teogeb 84775ae
move sum() to payoutProportionalStrategy.ts
teogeb 70cf31d
minimumStakingPeriodSeconds
teogeb 44028c2
maxAcceptableMinOperatorCount
teogeb 6b53523
maxOperators
teogeb b85a3d2
MIN_SPONSORSHIP_TOTAL_PAYOUT_PER_SECOND
teogeb 9a84781
improve tooSmallAdjustments removal
teogeb 5ddf14f
rm unnecessary variable
teogeb 3ddfc92
add comment
teogeb a1c87b6
rm unnecessary Number -> bigint type conversions
teogeb 9649cba
fix hasAcceptableOperatorCount() for sponsorships which we've already…
teogeb 7caa8f0
always unstake from expired sponsorship
teogeb 645a1ab
changelog
teogeb 29cd6ce
rename stakes -> myCurrentStakes
teogeb a51bbc7
rename unstakedAmount -> myUnstakedAmount
teogeb a2d4c3a
inline OperatorState interface
teogeb 3726cff
inline OperatorConfig and EnvironmentConfig interfaces
teogeb d0353de
AutostakerPluginConfig fields in same order as in adjustStakes()
teogeb 49e6d96
config defaults
teogeb eec128f
Merge branch 'main' into autostaker-plugin
teogeb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
207 changes: 207 additions & 0 deletions
207
packages/node/src/plugins/autostaker/AutostakerPlugin.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
import { _operatorContractUtils, SignerWithProvider, StreamrClient } from '@streamr/sdk' | ||
import { collect, Logger, scheduleAtApproximateInterval, TheGraphClient, WeiAmount } from '@streamr/utils' | ||
import { Schema } from 'ajv' | ||
import { formatEther, parseEther } from 'ethers' | ||
import { Plugin } from '../../Plugin' | ||
import PLUGIN_CONFIG_SCHEMA from './config.schema.json' | ||
import { adjustStakes } from './payoutProportionalStrategy' | ||
import { Action, SponsorshipConfig, SponsorshipID } from './types' | ||
|
||
export interface AutostakerPluginConfig { | ||
operatorContractAddress: string | ||
maxSponsorshipCount: number | ||
minTransactionDataTokenAmount: number | ||
maxAcceptableMinOperatorCount: number | ||
runIntervalInMs: number | ||
} | ||
|
||
interface SponsorshipQueryResultItem { | ||
id: SponsorshipID | ||
totalPayoutWeiPerSec: WeiAmount | ||
operatorCount: number | ||
maxOperators: number | null | ||
} | ||
|
||
interface StakeQueryResultItem { | ||
id: string | ||
sponsorship: { | ||
id: SponsorshipID | ||
} | ||
amountWei: WeiAmount | ||
} | ||
|
||
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<bigint> => { | ||
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, | ||
sponsorshipContractAddress: string, | ||
amount: WeiAmount | ||
) => Promise<void> => { | ||
switch (action.type) { | ||
case 'stake': | ||
return _operatorContractUtils.stake | ||
case 'unstake': | ||
return _operatorContractUtils.unstake | ||
default: | ||
throw new Error('assertion failed') | ||
} | ||
} | ||
|
||
export class AutostakerPlugin extends Plugin<AutostakerPluginConfig> { | ||
|
||
private abortController: AbortController = new AbortController() | ||
|
||
async start(streamrClient: StreamrClient): Promise<void> { | ||
logger.info('Start autostaker plugin') | ||
const minStakePerSponsorship = await fetchMinStakePerSponsorship(streamrClient.getTheGraphClient()) | ||
scheduleAtApproximateInterval(async () => { | ||
try { | ||
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, minStakePerSponsorship: bigint): Promise<void> { | ||
logger.info('Run autostaker analysis') | ||
const provider = (await streamrClient.getSigner()).provider | ||
const operatorContract = _operatorContractUtils.getOperatorContract(this.pluginConfig.operatorContractAddress) | ||
.connect(provider) | ||
const myCurrentStakes = await this.getMyCurrentStakes(streamrClient) | ||
const stakeableSponsorships = await this.getStakeableSponsorships(myCurrentStakes, streamrClient) | ||
const myStakedAmount = await operatorContract.totalStakedIntoSponsorshipsWei() | ||
const myUnstakedAmount = (await operatorContract.valueWithoutEarnings()) - myStakedAmount | ||
logger.debug('Analysis state', { | ||
stakeableSponsorships: [...stakeableSponsorships.entries()].map(([sponsorshipId, config]) => ({ | ||
sponsorshipId, | ||
payoutPerSec: formatEther(config.payoutPerSec) | ||
})), | ||
myCurrentStakes: [...myCurrentStakes.entries()].map(([sponsorshipId, amount]) => ({ | ||
sponsorshipId, | ||
amount: formatEther(amount) | ||
})), | ||
balance: { | ||
unstaked: formatEther(myUnstakedAmount), | ||
staked: formatEther(myStakedAmount) | ||
} | ||
}) | ||
const actions = adjustStakes({ | ||
myCurrentStakes, | ||
myUnstakedAmount, | ||
stakeableSponsorships, | ||
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) { | ||
logger.info(`Execute action: ${action.type} ${formatEther(action.amount)} ${action.sponsorshipId}`) | ||
await getStakeOrUnstakeFunction(action)(signer, | ||
this.pluginConfig.operatorContractAddress, | ||
action.sponsorshipId, | ||
action.amount | ||
) | ||
} | ||
} | ||
|
||
private async getStakeableSponsorships( | ||
stakes: Map<SponsorshipID, WeiAmount>, | ||
teogeb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
streamrClient: StreamrClient | ||
): Promise<Map<SponsorshipID, SponsorshipConfig>> { | ||
const queryResult = streamrClient.getTheGraphClient().queryEntities<SponsorshipQueryResultItem>((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: ` | ||
jtakalai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
sponsorships ( | ||
where: { | ||
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} | ||
) { | ||
id | ||
totalPayoutWeiPerSec | ||
operatorCount | ||
maxOperators | ||
} | ||
} | ||
` | ||
} | ||
}) | ||
const sponsorships = await collect(queryResult) | ||
const hasAcceptableOperatorCount = (item: SponsorshipQueryResultItem) => { | ||
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, { | ||
payoutPerSec: BigInt(sponsorship.totalPayoutWeiPerSec), | ||
}]) | ||
) | ||
} | ||
|
||
private async getMyCurrentStakes(streamrClient: StreamrClient): Promise<Map<SponsorshipID, WeiAmount>> { | ||
const queryResult = streamrClient.getTheGraphClient().queryEntities<StakeQueryResultItem>((lastId: string, pageSize: number) => { | ||
return { | ||
query: ` | ||
{ | ||
stakes ( | ||
where: { | ||
operator: "${this.pluginConfig.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) ])) | ||
} | ||
|
||
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 | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"$id": "config.schema.json", | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "object", | ||
"description": "Autostaker plugin configuration", | ||
"additionalProperties": false, | ||
"required": [ | ||
"operatorContractAddress" | ||
], | ||
"properties": { | ||
"operatorContractAddress": { | ||
"type": "string", | ||
"description": "Operator contract Ethereum address", | ||
"format": "ethereum-address" | ||
}, | ||
"maxSponsorshipCount": { | ||
"type": "integer", | ||
"description": "Maximum count of sponsorships which are staked at any given time", | ||
"minimum": 1, | ||
"default": 100 | ||
}, | ||
"minTransactionDataTokenAmount": { | ||
"type": "integer", | ||
"description": "Minimum data token amount for stake/unstake transaction", | ||
"minimum": 0, | ||
"default": 1000 | ||
}, | ||
"maxAcceptableMinOperatorCount": { | ||
"type": "integer", | ||
"description": "Maximum acceptable value for a sponsorship's minOperatorCount config option", | ||
"minimum": 0, | ||
"default": 100 | ||
}, | ||
"runIntervalInMs": { | ||
"type": "integer", | ||
"description": "The interval (in milliseconds) at which autostaking possibilities are analyzed and executed", | ||
"minimum": 0, | ||
"default": 3600000 | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.