- 
                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.