Skip to content

Instantly share code, notes, and snippets.

@inertia186
Last active January 9, 2021 23:11
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save inertia186/07cfb044f625beb22724371b85cea0e4 to your computer and use it in GitHub Desktop.
Save inertia186/07cfb044f625beb22724371b85cea0e4 to your computer and use it in GitHub Desktop.
Lucky Luke ('luckyluke.rb') for STEEM. Please have a look at the README.md file.
luckyluke.log
.rvmrc
flag_signals.txt │·
skip_accounts.txt │·
skip_tags.txt │·
vote_signals.txt │·
voters.txt
luckyluke-disabled-voters.txt
  • Title: luckyluke.rb - Voting Bot
  • Tags: radiator ruby steem steemdev curation
  • Notes:

Lucky Luke is a reimplementation of Dr. Phil, but instead of voting for new articles, it votes for posts mentioned in the memo field of a transfer operation. By default, it votes for any transfer sent to @booster but you can configure any bot that receives pay-for-vote transfers (or even @null). You can also set a minimum transfer amount to ignore small amounts.

New Features

  • vote_weight can now be either a static percentage (like 100.00 %) or dynamic. When using dynamic, the vote is compared to account history. If the transfer is equal to or greater than the maximum transfer in history, the vote will be 100%. Otherwise, the transfer amount is divided by the maximum. History is set by history_limit.
  • reserve_voting_power will allow the bot to exceed min_voting_power by the amount specified when a transfer is equal to or greater than the maximum transfer in history.
  • Added luckyluke-disabled-voters.txt to keep track of accounts that can no longer vote due to things like keys changing. This file may be appended to live in order to disable/enable voting on certain accounts without restarting the bot.

Features

  • YAML config.
    • voting_rules
      • min_transfer allows you to specify the minimum amount in the transfer to vote on.
      • min_wait and max_wait (in minutes) so that you can fine-tune voting delay.
      • enable_comments option to vote for post replies (default false).
      • max_rep option, useful for limiting votes to newer authors (default 99.9).
      • min_rep can accept either a static reputation or a dynamic property.
        • Existing static reputation still supported, e.g.: 25.0
        • Dynamic reputation, e.g.: dynamic:100. This will occasionally query the top 100 trending posts and use the minimum author reputation.
      • min_voting_power to create a floor with will allow the voter to recharge over time without having to stop the script.
      • vote_signals account list.
        • Optionally allows multiple bot instances to cooperate by avoiding vote swarms.
        • If enabled, this feature allows cooperation without sharing keys.
      • only_tags (optional) which only votes on posts that include these tags.
      • only_above_average_transfers allows voters to only vote if the transfer is above average for that bot.
      • history_limit used when only_above_average_transfers is true or vote_weight is dynamic to set how far back to calculate.
      • max_transfer allows you to specify the maximum amount in the transfer to vote on, which is useful when running multiple instances with voting tiers.
      • max_age allows you to only upvote newer content, for example, avoiding posts that are about to become locked.
      • Optionally configure voters as a separate filename. E.g:
        • voters: voters.txt
          • The format for the file is just: account wif (no leading dash, separated by space)
        • Or continue to use the previous format.
      • Also optional support for separate files in each (format one per line or separated by space or both):
        • skip_accounts
        • skip_tags
        • flag_signals
        • vote_signals
  • bots is a list of bots to watch transfer operations for.
  • Skip posts with declined payout.
  • Skip posts that already have votes from external scripts and posts that were edited.
  • Argument called replay: allows a replay of n blocks allowing you to catch up to the present.
    • E.g.: ruby luckyluke.rb replay:90 will replay the last 90 blocks (about 4.5 minutes).
  • Thread management
    • Counter displayed so you know what kind of impact ^C will have.
    • This also keeps the number of threads down when authors edit before Lucky Luke votes.
  • Streaming on Last Irreversible Block Number, just to be fancy.
  • Checking for new HF18 cashout_time value (if present).
    • This will skip voting when authors edit their old archived posts.

Overview

The goal is to vote before the pay-for-vote bot. To achieve this, Lucky Luke watches for transfer operations.

You might configure the bot to only watch for transfers over 10.000 SBD, for example. The bot will also use a few other rules like to avoid voting for declined payouts and automatically suspend voting if it needs to recharge.


Install

To use this Radiator bot:

Linux
$ sudo apt-get update
$ sudo apt-get install ruby-full git openssl libssl1.0.0 libssl-dev
$ sudo apt-get upgrade
$ gem install bundler
macOS
$ gem install bundler

You can try the system version of ruby, but if you have issues with that, use this how-to, and come back to this installation at Step 4:

I've tested it on various versions of ruby. The oldest one I got it to work was:

ruby 2.0.0p645 (2015-04-13 revision 50299) [x86_64-darwin14.4.0]

Setup

First, clone this gist and install the dependencies:

$ git clone https://gist.github.com/07cfb044f625beb22724371b85cea0e4.git luckyluke
$ cd luckyluke
$ bundle install

Then run it:

$ ruby luckyluke.rb

Lucky Luke will now do it's thing. Check here to see an updated version of this bot:

https://gist.github.com/inertia186/07cfb044f625beb22724371b85cea0e4


Upgrade

Typically, you can upgrade to the latest version by this command, from the original directory you cloned into:

$ git pull

Usually, this works fine as long as you haven't modified anything. If you get an error, try this:

$ git stash --all
$ git pull --rebase
$ git stash pop

If you're still having problems, I suggest starting a new clone.


Troubleshooting

Problem: What does this error mean?
luckyluke.yml:1: syntax error, unexpected ':', expecting end-of-input
Solution: You ran ruby luckyluke.yml but you should run ruby luckyluke.rb.

Problem: Everything looks ok, but every time Lucky Luke tries to vote, I get this error:
Unable to vote with <account>.  Invalid version
Solution: You're trying to vote with an invalid key.

Make sure the .yml file voter items have the account name, followed by a space, followed by the account's WIF posting key. Also make sure you have removed the example accounts (social and bad.account are just for testing).

Problem: The node I'm using is down.

Is there a list of nodes?

Solution: Yes, see:

https://steem-third-party.github.io/steem-servers/


See my previous Ruby How To posts in: /f/ruby

Get in touch!

If you're using Lucky Luke, I'd love to hear from you. Drop me a line and tell me what you think! I'm @inertia on STEEM and SteemSpeak.

License

I don't believe in intellectual "property". If you do, consider Lucky Luke as licensed under a Creative Commons CC0 License.

{
"name": "Lucky Luke",
"description": "This bot votes for posts mentioned in the memo field of a transfer operation. By default, it votes for any transfer sent to @booster but you can configure any bot that receives pay-for-vote transfers (or even @null).",
"image": "heroku/ruby",
"repository": "https://gist.github.com/07cfb044f625beb22724371b85cea0e4",
"keywords": ["radiator", "ruby", "steem", "steemit", "steemdev", "curation"],
"env": {
"STEEM_USER": "social",
"POSTING_KEY_PRV": "5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC",
"BOTS": "booster"
}
}
#!/bin/bash
# This script is suitable to run luckyluke in the background so that you can
# disconnect from the terminal or use it as a monit process group.
#
# Asumptions, you've ...
#
# 1. ... cloned to ~/luckyluke.
# 2. ... already run: bundle install
# 3. ... updated luckylike.yml.
# 4. ... installed rvm and ruby-2.4.2 (if not, see: https://rvm.io).
cd $HOME/luckyluke
current_pid="`/bin/cat $HOME/luckyluke/luckyluke.pid`"
# Stop the previous run, if present.
if [ "`/bin/ps -ef | /usr/bin/awk '$NF~"luckyluke" {print $2}'`" -eq $current_pid ]; then
/bin/kill $current_pid
fi
[[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm"
$HOME/.rvm/scripts/rvm use ruby-2.4.2
# Start a new process.
/usr/bin/nohup $HOME/.rvm/rubies/ruby-2.4.2/bin/ruby luckyluke.rb > luckyluke.log 2>&1 & echo $! > $HOME/luckyluke/nohup.pid
source 'https://rubygems.org'
gem 'radiator'
gem 'awesome_print'
GEM
remote: https://rubygems.org/
specs:
awesome_print (1.8.0)
bitcoin-ruby (0.0.13)
connection_pool (2.2.1)
ffi (1.9.18)
hashie (3.5.6)
json (2.1.0)
little-plugger (1.1.4)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
multi_json (1.12.2)
net-http-persistent (3.0.0)
connection_pool (~> 2.2)
radiator (0.3.12)
awesome_print (~> 1.7, >= 1.7.0)
bitcoin-ruby (~> 0.0, >= 0.0.11)
ffi (~> 1.9, >= 1.9.18)
hashie (~> 3.5, >= 3.5.5)
json (~> 2.0, >= 2.0.2)
logging (~> 2.2, >= 2.2.0)
net-http-persistent (>= 2.5.2)
PLATFORMS
ruby
DEPENDENCIES
awesome_print
radiator
BUNDLED WITH
1.16.0.pre.3
:voting_rules:
:vote_weight: dynamic
:min_transfer: 0.001 GBG
# :max_transfer: 100.000 GBG
:only_above_average_transfers: false
:history_limit: 1000
:enable_comments: false
:min_wait: 18
:max_wait: 30
:min_rep: 25.0
:max_rep: 99.9
:min_voting_power: 25.00 %
:reserve_voting_power: 2.00 %
:max_age: 1440
:voters:
- social 5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
- bad.account 5XXXBadWifXXXdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
:bots: booster
:skip_accounts: leeroy.jenkins the.masses danlarimer ned-reddit-login
:skip_tags: nsfw test ru--mat bm-open
# :only_tags: steemit
# :only_apps: steemit esteem streemian pysteem steepshot busy chainbb banjo_bot chronicle steemq
# :skip_apps: piston
:flag_signals: cheetah kulturagolosa
:vote_signals:
:chain_options:
:chain: golos
:url: https://ws.golos.io
# Lucky Luke (luckyluke) watches transfers sent to pay-for-vote bots and tries
# to vote for the content first (front-running).
#
# See: https://steemit.com/radiator/@inertia/luckyluke-rb-voting-bot
require 'rubygems'
require 'bundler/setup'
require 'yaml'
# require 'irb'
Bundler.require
defined? Thread.report_on_exception and Thread.report_on_exception = true
# If there are problems, this is the most time we'll wait (in seconds).
MAX_BACKOFF = 12.8
VOTE_RECHARGE_PER_DAY = 20.0
VOTE_RECHARGE_PER_HOUR = VOTE_RECHARGE_PER_DAY / 24
VOTE_RECHARGE_PER_MINUTE = VOTE_RECHARGE_PER_HOUR / 60
VOTE_RECHARGE_PER_SEC = VOTE_RECHARGE_PER_MINUTE / 60
@config_path = __FILE__.sub(/\.rb$/, '.yml')
@disabled_voter_path = __FILE__.sub(/\.rb$/, '-disabled-voters.txt')
@account_history = {}
unless File.exist? @config_path
puts "Unable to find: #{@config_path}"
exit
end
def parse_voters(voters)
case voters
when String
raise "Not found: #{voters}" unless File.exist? voters
hash = {}
File.open(voters, 'r').each do |line|
key, value = line.split(' ')
hash[key] = value if !!key && !!hash
end
hash
when Array
a = voters.map{ |v| v.split(' ')}.flatten.each_slice(2)
return a.to_h if a.respond_to? :to_h
hash = {}
voters.each_with_index do |e|
key, val = e.split(' ')
hash[key] = val
end
hash
else; raise "Unsupported voters: #{voters}"
end
end
def parse_list(list)
if !!list && File.exist?(list)
elements = []
File.open(list, 'r').each do |line|
elements += line.split(' ')
end
elements.uniq.reject(&:empty?).reject(&:nil?)
else
list.to_s.split(' ')
end
end
def parse_slug(slug)
slug = slug.downcase.split('@').last
return [] if slug.nil?
author_name = slug.split('/')[0]
permlink = slug.split('/')[1..-1].join('/')
permlink = permlink.split('?')[0]
unless permlink.nil?
permlink = permlink.sub(/\/$/, '')
permlink = permlink.sub(/#comments$/, '')
end
[author_name, permlink]
end
@config = YAML.load_file(@config_path)
rules = @config[:voting_rules]
@voting_rules = {
vote_weight: rules[:vote_weight],
min_transfer: rules[:min_transfer],
min_transfer_asset: rules[:min_transfer].to_s.split(' ').last,
min_transfer_amount: rules[:min_transfer].to_s.split(' ').first.to_f,
alternative_assets: rules[:alternative_assets].to_s.split(' '),
max_transfer: rules[:max_transfer],
max_transfer_asset: rules[:max_transfer].to_s.split(' ').last,
max_transfer_amount: rules[:max_transfer].to_s.split(' ').first.to_f,
only_above_average_transfers: rules[:only_above_average_transfers],
history_limit: rules[:history_limit].to_i,
enable_comments: rules[:enable_comments],
min_wait: rules[:min_wait].to_i,
max_wait: rules[:max_wait].to_i,
min_voting_power: (((rules[:min_voting_power] || '0.0 %').to_f) * 100).to_i,
reserve_voting_power: (((rules[:reserve_voting_power] || '0.0 %').to_f) * 100).to_i,
max_age: rules[:max_age].to_i,
}
unless @voting_rules[:vote_weight] == 'dynamic'
@voting_rules[:vote_weight] = (((@voting_rules[:vote_weight] || '100.0 %').to_f) * 100).to_i
end
@voting_rules[:wait_range] = [@voting_rules[:min_wait]..@voting_rules[:max_wait]]
unless @voting_rules[:min_rep] =~ /dynamic:[0-9]+/
@voting_rules[:min_rep] = @voting_rules[:min_rep].to_f
end
@voting_rules = Struct.new(*@voting_rules.keys).new(*@voting_rules.values)
@voters = parse_voters(@config[:voters])
@bots = parse_list(@config[:bots])
@skip_accounts = parse_list(@config[:skip_accounts])
@skip_tags = parse_list(@config[:skip_tags])
@only_tags = parse_list(@config[:only_tags])
@skip_apps = parse_list(@config[:skip_apps])
@only_apps = parse_list(@config[:only_apps])
@flag_signals = parse_list(@config[:flag_signals])
@vote_signals = parse_list(@config[:vote_signals])
@options = @config[:chain_options]
@options[:logger] = Logger.new(__FILE__.sub(/\.rb$/, '.log'))
@voted_for_authors = {}
@voting_power = {}
@api = nil
@follow_api = nil
@stream = nil
@threads = nil
@semaphore = Mutex.new
def to_rep(raw)
raw = raw.to_i
neg = raw < 0
level = Math.log10(raw.abs)
level = [level - 9, 0].max
level = (neg ? -1 : 1) * level
level = (level * 9) + 25
level
end
def disabled_voters
disabled_voters = []
if File.exist? @disabled_voter_path
@disabled_voters_h ||= File.open(@disabled_voter_path, 'r')
@disabled_voters_h.rewind
@disabled_voters_h.each do |line|
disabled_voters << line.split(' ').first
end
end
disabled_voters
end
def active_voters
@voters.keys - disabled_voters
end
def poll_voting_power
@semaphore.synchronize do
@api.get_accounts(active_voters) do |accounts|
if accounts.size == 0
@min_voting_power = 0
@max_voting_power = 0
@average_voting_power = 0
return 0
end
accounts.each do |account|
voting_power = account.voting_power / 100.0
last_vote_time = Time.parse(account.last_vote_time + 'Z')
voting_elapse = Time.now.utc - last_vote_time
current_voting_power = voting_power + (voting_elapse * VOTE_RECHARGE_PER_SEC)
wasted_voting_power = [current_voting_power - 100.0, 0.0].max
current_voting_power = ([100.0, current_voting_power].min * 100).to_i
if wasted_voting_power > 0
puts "\t#{account.name} wasted voting power: #{('%.2f' % wasted_voting_power)} %"
end
@voting_power[account.name] = current_voting_power
end
@min_voting_power = @voting_power.values.min
@max_voting_power = @voting_power.values.max
@average_voting_power = @voting_power.values.reduce(0, :+) / accounts.size
end
end
end
def summary_voting_power
poll_voting_power
vp = @average_voting_power / 100.0
summary = []
summary << if @voting_power.size > 1
"Average remaining voting power: #{('%.3f' % vp)} %"
else
"Remaining voting power: #{('%.3f' % vp)} %"
end
if @voting_power.size > 1 && @max_voting_power > @voting_rules.min_voting_power
vp = @max_voting_power / 100.0
summary << "highest account: #{('%.3f' % vp)} %"
end
vp = @voting_rules.min_voting_power / 100.0
summary << "recharging when below: #{('%.3f' % vp)} %"
if @voting_rules.reserve_voting_power > 0
summary << "reserve voting power: #{'%.3f' % (@voting_rules.reserve_voting_power / 100.0)} %"
end
summary.join('; ')
end
def voters_recharging(weight)
target_voting_power = if weight < 10000
@voting_rules.min_voting_power
else
@voting_rules.min_voting_power - @voting_rules.reserve_voting_power
end
@voting_power.map do |voter, power|
voter if power < target_voting_power
end.compact
end
def account_history(bot)
@account_history[bot] = nil if rand < 0.05
limit = @voting_rules.history_limit
if @account_history[bot].nil?
args = [bot, -limit, limit]
@account_history[bot] = @api.get_account_history(*args) do |history, error|
history unless !!error
end
else
limit = (limit / 10).to_i
if limit > 0
args = [bot, -limit, limit]
@account_history[bot] += @api.get_account_history(*args) do |history, error|
history unless !!error
end
@account_history[bot] = @account_history[bot].uniq
end
end
@account_history[bot]
end
def average_transfer(bot, asset)
inputs = account_history(bot).map do |index, transaction|
type, op = transaction.op
next unless type == 'transfer'
next unless op.to == bot
next unless op.amount =~ /#{asset}$/
op.amount.split(' ').first.to_f
end.compact
sum = inputs.reduce(0, :+)
return true if sum == 0.0
sum / inputs.size
end
def above_average_transfer?(bot, amount, asset)
amount > average_transfer(bot, asset)
end
def max_transfer(bot, asset)
inputs = account_history(bot).map do |index, transaction|
type, op = transaction.op
next unless type == 'transfer'
next unless op.to == bot
next unless op.amount =~ /#{asset}$/
op.amount.split(' ').first.to_f
end.compact
inputs.max || 0.0
end
def skip_tags_intersection?(json_metadata)
metadata = JSON[json_metadata || '{}'] rescue []
tags = metadata['tags'] || [] rescue []
tags = [tags].flatten
(@skip_tags & tags).any?
end
def only_tags_intersection?(json_metadata)
return true if @only_tags.none? # not set, assume all tags intersect
metadata = JSON[json_metadata || '{}'] rescue []
tags = metadata['tags'] || [] rescue []
tags = [tags].flatten
(@only_tags & tags).any?
end
def skip_app?(json_metadata)
metadata = JSON[json_metadata || '{}'] rescue ''
app = metadata['app'].to_s.split('/').first
@skip_apps.include? app
end
def only_app?(json_metadata)
return true if @only_apps.none?
metadata = JSON[json_metadata || '{}'] rescue ''
app = metadata['app'].to_s.split('/').first
@only_apps.include? app
end
def base_to_debt_ratio
@last_base_to_debt_ratio = @market_history_api.get_ticker do |ticker|
latest = ticker.latest.to_f
bid = ticker.highest_bid.to_f
ask = ticker.lowest_ask.to_f
[latest, bid, ask].reduce(0, :+) / 3.0
end
rescue => e
puts "Unable to query market data: #{e}"
@market_history_api.shutdown
@market_history_api = Radiator::MarketHistoryApi.new(@options)
ensure
@last_base_to_debt_ratio || 1.0
end
def convert_asset(amount, asset)
original_amount = amount
original_asset = asset
amount = amount.to_f
ratio = base_to_debt_ratio
amount = case asset
when 'STEEM', 'GOLOS'
amount * ratio
when 'SBD', 'GBG'
amount / ratio
else
puts "Unsupported asset for bid: #{asset}"
0.0
end
asset = @voting_rules.min_transfer_asset
puts "Evaluating bid at #{original_amount} #{original_asset} as #{amount} #{asset} (ratio: #{ratio})"
[amount, asset]
end
def valid_transfer?(transfer)
to = transfer.to
amount = transfer.amount.split(' ').first.to_f
asset = transfer.amount.split(' ').last
return false unless @bots.include? to
if @voting_rules.alternative_assets.include? asset
amount, asset = convert_asset amount, asset
end
return false if @voting_rules.only_above_average_transfers && !above_average_transfer?(to, amount, asset)
if !@voting_rules.min_transfer.nil?
return false unless asset == @voting_rules.min_transfer_asset
return false unless amount >= @voting_rules.min_transfer_amount
end
if !@voting_rules.max_transfer.nil?
return false unless asset == @voting_rules.max_transfer_asset
return false unless amount <= @voting_rules.max_transfer_amount
end
true
end
# The rationale here is to find out if the bots have already voted because
# there's no way to front-run if this happens, so we need to know if this
# comment should be skipped.
def bots_already_voted?(comment)
all_voters = comment.active_votes.map(&:voter)
(all_voters & @bots).any?
end
def may_vote?(comment)
return false if !@voting_rules.enable_comments && !comment.parent_author.empty?
return false if @skip_tags.include? comment.parent_permlink
return false if skip_tags_intersection? comment.json_metadata
return false unless only_tags_intersection? comment.json_metadata
return false if @skip_accounts.include? comment.author
return false if skip_app? comment.json_metadata
return false unless only_app? comment.json_metadata
return false if bots_already_voted?(comment)
true
end
def min_trending_rep(limit)
begin
@semaphore.synchronize do
if @min_trending_rep.nil? || Random.rand(0..limit) == 13
puts "Looking up trending up to #{limit} transfers."
response = @api.get_discussions_by_trending(tag: '', limit: limit)
raise response.error.message if !!response.error
trending = response.result
@min_trending_rep = trending.map do |c|
c.author_reputation.to_i
end.min
puts "Current minimum dynamic rep: #{('%.3f' % to_rep(@min_trending_rep))}"
end
end
rescue => e
puts "Warning: #{e}"
end
@min_trending_rep || 0
end
def skip?(comment, voters)
if bots_already_voted?(comment)
puts "Skipped, cannot front-run:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
if comment.respond_to? :cashout_time # HF18
if (cashout_time = Time.parse(comment.cashout_time + 'Z')) < Time.now.utc
puts "Skipped, cashout time has passed (#{cashout_time}):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
end
if ((Time.now.utc - (created = Time.parse(comment.created + 'Z'))).to_i / 60) > @voting_rules.max_age
puts "Skipped, too old (#{created}):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
if comment.max_accepted_payout.split(' ').first == '0.000'
puts "Skipped, payout declined:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
if active_voters.empty?
puts "Skipped, everyone already voted:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
downvoters = comment.active_votes.map do |v|
v.voter if v.percent < 0
end.compact
if (signal = downvoters & @flag_signals).any?
# ... Got a signal flag ...
puts "Skipped, flag signals (#{signals.join(' ')} flagged):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
upvoters = comment.active_votes.map do |v|
v.voter if v.percent > 0
end.compact
if (signals = upvoters & @vote_signals).any?
# ... Got a signal vote ...
puts "Skipped, vote signals (#{signals.join(' ')} voted):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
all_voters = comment.active_votes.map(&:voter)
if (all_voters & voters).any?
# ... Someone already voted (probably because post was edited) ...
puts "Skipped, already voted:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
false
end
def vote_weight(transfer)
return @voting_rules.vote_weight unless @voting_rules.vote_weight == 'dynamic'
bot = transfer.to
amount, asset = transfer.amount.split(' ')
amount, asset = if @voting_rules.alternative_assets.include? asset
convert_asset amount, asset
else
[amount.to_f, asset]
end
max = max_transfer(bot, asset)
if amount >= max
10000 # full vote
else
((amount / max) * 10000).to_i
end
end
def async_vote(comment, wait_offset, transfer)
slug = "@#{comment.author}/#{comment.permlink}"
@threads.each do |k, t|
@threads.delete(k) unless t.alive?
end
@semaphore.synchronize do
if @threads.size != @last_threads_size
print "Pending votes: #{@threads.size} ... "
@last_threads_size = @threads.size
end
end
if @threads.keys.include? slug
puts "Skipped, vote already pending:\n\t#{slug}"
return
end
@threads[slug] = Thread.new do
vote(comment, wait_offset, transfer)
end
end
def vote(comment, wait_offset, transfer)
votes_cast = 0
backoff = 0.2
slug = "@#{comment.author}/#{comment.permlink}"
weight = vote_weight(transfer)
voters = active_voters - comment.active_votes.map(&:voter) - voters_recharging(weight)
return if skip?(comment, voters)
if wait_offset == 0
timestamp = Time.parse(comment.created + ' Z')
now = Time.now.utc
wait_offset = now - timestamp
end
if (wait = (Random.rand(*@voting_rules.wait_range) * 60) - wait_offset) > 0
puts "Waiting #{wait.to_i} seconds to vote for:\n\t#{slug}"
sleep wait
response = @api.get_content(comment.author, comment.permlink)
comment = response.result
return if skip?(comment, voters)
else
puts "Catching up to vote for:\n\t#{slug}"
sleep 3
end
loop do
begin
break if voters.empty?
author = comment.author
permlink = comment.permlink
voter = voters.sample
break if weight == 0.0
if (vp = @voting_power[voter].to_i) < @voting_rules.min_voting_power
vp = vp / 100.0
if active_voters.size > 1
puts "Recharging #{voter} vote power (currently too low: #{('%.3f' % vp)} %)"
else
puts "Recharging vote power (currently too low: #{('%.3f' % vp)} %)"
end
end
wif = @voters[voter]
tx = Radiator::Transaction.new(@options.merge(wif: wif, pool_size: 1))
puts "#{voter} voting for #{slug} (transferred #{transfer.amount} to get #{(weight / 100.0)} % upvote)"
vote = {
type: :vote,
voter: voter,
author: author,
permlink: permlink,
weight: weight
}
tx.operations << vote
response = tx.process(true)
if !!response.error
message = response.error.message
if message.to_s =~ /You have already voted in a similar way./
puts "\tFailed: duplicate vote."
voters -= [voter]
next
elsif message.to_s =~ /Can only vote once every 3 seconds./
puts "\tRetrying: voting too quickly."
sleep 3
next
elsif message.to_s =~ /Voting weight is too small, please accumulate more voting power or steem power./
puts "\tFailed: voting weight too small"
voters -= [voter]
next
elsif message.to_s =~ /tx_missing_posting_auth: missing required posting authority/
puts "\tFailed: missing required posting authority (#{voter})"
disable_voter voter, 'missing required posting authority'
voters -= [voter]
next
elsif message.to_s =~ /STEEMIT_UPVOTE_LOCKOUT_HF17/
puts "\tFailed: upvote lockout (last twelve hours before payout)"
break
elsif message.to_s =~ /tapos_block_summary/
puts "\tRetrying: tapos_block_summary (?)"
redo
elsif message.to_s =~ /now < trx.expiration/
puts "\tRetrying: now < trx.expiration (?)"
redo
elsif message.to_s =~ /transaction_expiration_exception: transaction expiration exception/
puts "\tRetrying: transaction_expiration_exception: transaction expiration exception"
redo
elsif message.to_s =~ /signature is not canonical/
puts "\tRetrying: signature was not canonical (bug in Radiator?)"
redo
end
ap response
raise message
else
voters -= [voter]
end
puts "\tSuccess: #{response.result.to_json}"
@voted_for_authors[author] = Time.now.utc
votes_cast += 1
next
rescue => e
puts "Pausing #{backoff} :: Unable to vote with #{voter}. #{e.class} :: #{e}"
disable_voter voter, 'bad wif' if e.inspect =~ /Invalid version/i
voters -= [voter]
sleep backoff
backoff = [backoff * 2, MAX_BACKOFF].min
end
end
end
def disable_voter(voter, reason)
return if disabled_voters.include? voter
File.open(@disabled_voter_path, 'a+') do |f|
f.puts "#{voter} # #{reason}"
end
end
puts "Accounts voting: #{active_voters.size}"
replay = 0
ARGV.each do |arg|
if arg =~ /replay:[0-9]+/
replay = arg.split('replay:').last.to_i rescue 0
end
end
if replay > 0
Thread.new do
@api ||= Radiator::Api.new(@options)
@follow_api ||= Radiator::FollowApi.new(@options)
@stream ||= Radiator::Stream.new(@options)
properties = @api.get_dynamic_global_properties.result
last_irreversible_block_num = properties.last_irreversible_block_num
block_number = last_irreversible_block_num - replay
puts "Replaying from block number #{block_number} ..."
@api.get_blocks(block_number..last_irreversible_block_num) do |block, number|
next unless !!block
timestamp = Time.parse(block.timestamp + ' Z')
now = Time.now.utc
elapsed = now - timestamp
block.transactions.each do |tx|
tx.operations.each do |type, op|
if type == 'transfer' && valid_transfer?(op)
author, permlink = parse_slug(op.memo)
next if author.nil? || permlink.nil?
comment = @api.get_content(author, permlink).result
if may_vote?(comment)
async_vote(comment, elapsed.to_i, op)
end
end
end
end
end
sleep 3
puts "Done replaying."
end
end
puts "Now watching for new transfers to: #{@bots.join(', ')}"
loop do
@api ||= Radiator::Api.new(@options)
@follow_api ||= Radiator::FollowApi.new(@options)
@stream ||= Radiator::Stream.new(@options)
@market_history_api ||= @market_history_api = Radiator::MarketHistoryApi.new(@options)
@threads ||= {}
op_idx = 0
begin
puts summary_voting_power
counter = 0
@stream.operations(:transfer) do |transfer|
next unless valid_transfer? transfer
author, permlink = parse_slug(transfer.memo)
next if author.nil? || permlink.nil?
comment = @api.get_content(author, permlink).result
next unless may_vote? comment
if @max_voting_power < @voting_rules.min_voting_power
vp = @max_voting_power / 100.0
puts "Recharging vote power (currently too low: #{('%.3f' % vp)} %)"
if disabled_voters.any?
puts "Disabled voters: #{disabled_voters.size}"
end
end
async_vote(comment, 0, transfer)
puts summary_voting_power
end
rescue => e
@api.shutdown
@api = nil
@follow_api.shutdown
@follow_api = nil
@market_history_api.shutdown
@market_history_api = nil
@stream.shutdown
@stream = nil
@threads = nil
puts "Unable to stream on current node. Retrying in 5 seconds. Error: #{e}"
puts e.backtrace
sleep 5
end
end
:voting_rules:
:vote_weight: dynamic
:min_transfer: 0.001 SBD
:alternative_assets: STEEM
# :max_transfer: 100.000 SBD
:only_above_average_transfers: false
:history_limit: 1000
:enable_comments: false
:min_wait: 18
:max_wait: 30
:min_rep: 25.0
:max_rep: 99.9
:min_voting_power: 25.00 %
:reserve_voting_power: 2.00 %
:max_age: 7200
:voters:
- social 5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
- bad.account 5XXXBadWifXXXdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
:bots: booster
:skip_accounts: leeroy.jenkins the.masses danlarimer ned-reddit-login
:skip_tags: nsfw test
# :only_tags: steemit
# :only_apps: steemit esteem streemian pysteem steepshot busy chainbb banjo_bot chronicle steemq
# :skip_apps: piston
:flag_signals: cheetah steemcleaners
:vote_signals:
:chain_options:
:chain: steem
:url: https://api.steemit.com
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment