base/lib/stellar/claim_predicate.rb
# frozen_string_literals: true
require "active_support/core_ext/integer/time"
require "active_support/core_ext/string/conversions"
module Stellar
# Represents claim predicate on Stellar network.
#
# @see https://developers.stellar.org/docs/glossary/claimable-balance/
class ClaimPredicate
module FactoryMethods
# Constructs an `unconditional` claim predicate.
#
# This predicate will be always fulfilled.
#
# @return [ClaimPredicate] `unconditional` claim predicate
def unconditional
ClaimPredicate.new(ClaimPredicateType::UNCONDITIONAL)
end
# Constructs a `before_relative_time` claim predicate.
#
# This predicate will be fulfilled if the closing time of the ledger that includes
# the Stellar::CreateClaimableBalance operation plus this relative time delta (in seconds)
# is less than the current time.
#
# @param seconds [#to_int|#to_i] seconds since `closeTime` of the ledger in which
# the ClaimableBalanceEntry was created.
# @return [ClaimPredicate] `before_relative_time` claim predicate
def before_relative_time(seconds)
ClaimPredicate.new(ClaimPredicateType::BEFORE_RELATIVE_TIME, Integer(seconds))
end
# Constructs an `before_absolute_time` claim predicate.
#
# This predicate will be fulfilled if the closing time of the ledger that includes
# the Stellar::CreateClaimableBalance operation is less than provided timestamp.
#
# @param timestamp [#to_time|#to_int|#to_i] time value or timestamp
#
# @return [ClaimPredicate] `before_absolute_time` claim predicate.
def before_absolute_time(timestamp)
ts = timestamp.respond_to?(:to_time) ? timestamp.to_time : timestamp
raise ArgumentError, "'#{timestamp}' is not a valid timestamp" if timestamp && ts.nil?
ClaimPredicate.new(ClaimPredicateType::BEFORE_ABSOLUTE_TIME, Integer(ts))
end
# Constructs either relative or absolute time predicate based on the type of the input.
#
# If input is an instance of `ActiveSupport::Duration` class it will be handled as a relative time
# (seconds since close time of the ledger), otherwise it will be treated as an absolute time.
#
# It is intended to work with time helpers provided by ActiveSupport, like `1.day` (relative)
# or `2.weeks.from_now` (absolute).
#
# @example relative time
# ClaimPredicate.before(2.days + 15.seconds)
#
# @example absolute time
# ClaimPredicate.before(5.hours.from_now)
#
# @param time [ActiveSupport::Duration|#to_time|#to_i] duration since ledger close time or absolute time value
#
# @return [ClaimPredicate] `before_relative_time` or `before_absolute_time` claim predicate.
def before(time)
(ActiveSupport::Duration === time) ? before_relative_time(time.to_i) : before_absolute_time(time)
end
# Constructs a negated predicate from either relative or absolute time based on the type of the input.
#
# @see #before
# @param (see #before)
# @return (see #before)
def after(time)
~before(time)
end
# Compose a complex predicate by calling DSL methods from the block.
#
# @example
# ClaimPredicate.compose {
# after(15.minutes) & before(1.day) | after(1.week.from_now) & before(1.week.from_now + 1.day)
# }
#
# @yieldreturn [ClaimPredicate|nil]
# @return [ClaimPredicate] the result of executing the block in context of ClaimPredicate
def compose(&block)
result = instance_eval(&block)
result.nil? ? unconditional : result
end
end
extend FactoryMethods
# Constructs an `and` claim predicate.
#
# This predicate will be fulfilled if both `self` and `other` predicates are fulfilled.
#
# @param other [ClaimPredicate] another predicate
#
# @return [ClaimPredicate] `and` claim predicate
def and(other)
raise TypeError, "no conversion from #{other.class.name} to ClaimPredicate" unless ClaimPredicate === other
ClaimPredicate.new(ClaimPredicateType::AND, [self, other])
end
alias_method :&, :and
# Constructs an `or` claim predicate.
#
# This predicate will be fulfilled if either of `self` or `other` predicates is fulfilled.
#
# @param other [ClaimPredicate] another predicate.
#
# @return [ClaimPredicate] `or` claim predicate
def or(other)
raise TypeError, "no conversion from #{other.class.name} to ClaimPredicate" unless ClaimPredicate === other
ClaimPredicate.new(ClaimPredicateType::OR, [self, other])
end
alias_method :|, :or
# Constructs a `not` claim predicate.
#
# This predicate will be fulfilled if `self` is not fulfilled.
#
# @return [ClaimPredicate] `not` claim predicate
def not
ClaimPredicate.new(ClaimPredicateType::NOT, self)
end
alias_method :~@, :not
# Evaluates the predicate value for provided inputs.
#
# @param created_at [#to_time|#to_int] closing time of the ledger containing CreateClaimableBalance operation
# @param claiming_at [#to_time|#to_int|ActiveSupport::Duration] time point to evaluate predicate at, either
# absolute time or duration relative to `created_at`. In reality predicate will be evaluated by stellar-core
# using the closing time of a ledger containing ClaimClaimableBalance operation, in either successful
# or failed state.
#
# @return [Boolean] `true` if this predicate would allow claiming the balance, `false` otherwise
def evaluate(created_at, claiming_at)
created_at = created_at.to_time if created_at.respond_to?(:to_time)
claiming_at = created_at + claiming_at if claiming_at.is_a?(ActiveSupport::Duration)
claiming_at = claiming_at.to_time if claiming_at.respond_to?(:to_time)
return false if claiming_at < created_at
case switch
when ClaimPredicateType::UNCONDITIONAL
true
when ClaimPredicateType::BEFORE_RELATIVE_TIME
Integer(claiming_at) < Integer(created_at) + value
when ClaimPredicateType::BEFORE_ABSOLUTE_TIME
Integer(claiming_at).to_i < value
when ClaimPredicateType::AND
value[0].evaluate(created_at, claiming_at) && value[1].evaluate(created_at, claiming_at)
when ClaimPredicateType::OR
value[0].evaluate(created_at, claiming_at) || value[1].evaluate(created_at, claiming_at)
when ClaimPredicateType::NOT
!value.evaluate(created_at, claiming_at)
else
raise ArgumentError, "evaluation is not implemented for #{switch.name} predicate"
end
end
def describe
case switch
when ClaimPredicateType::UNCONDITIONAL
"always"
when ClaimPredicateType::BEFORE_RELATIVE_TIME
dur = ActiveSupport::Duration.build(value)
"less than #{dur.inspect} since creation"
when ClaimPredicateType::BEFORE_ABSOLUTE_TIME
"before #{Time.at(value).to_formatted_s(:db)}"
when ClaimPredicateType::AND
value.map(&:describe).join(" and ")
when ClaimPredicateType::OR
"(" << value.map(&:describe).join(" or ") << ")"
when ClaimPredicateType::NOT
case value.switch
when ClaimPredicateType::UNCONDITIONAL
"never"
when ClaimPredicateType::BEFORE_RELATIVE_TIME
dur = ActiveSupport::Duration.build(value.value)
"#{dur.inspect} or more since creation"
when ClaimPredicateType::BEFORE_ABSOLUTE_TIME
"after #{Time.at(value.value).to_formatted_s(:db)}"
else
"not (#{value.describe})"
end
else
raise ArgumentError, "evaluation is not implemented for #{switch.name} predicate"
end
end
def inspect
"#<ClaimPredicate: #{describe}>"
end
end
end