From 6f23ceedff49f1b8bda46af41b32ace583f2ecda Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Fri, 25 Apr 2025 19:21:49 +0300 Subject: [PATCH] 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 + } + } +}