require "bigdecimal" module Stellar class Operation MAX_INT64 = 2**63 - 1 class << self # # Construct a new Stellar::Operation from the provided # source account and body # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :source_account # @option attributes [Stellar::Operation::Body] :body # # @return [Stellar::Operation] the built operation def make(attributes = {}) source_account = attributes[:source_account] body = Stellar::Operation::Body.new(*attributes[:body]) op = Stellar::Operation.new(body: body) if source_account raise ArgumentError, "Bad :source_account" unless source_account.is_a?(Stellar::KeyPair) op.source_account = source_account.muxed_account end op end # # Helper method to create a valid PaymentOp, wrapped # in the necessary XDR structs to be included within a # transactions `operations` array. # # @see Stellar::Asset # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :destination the receiver of the payment # @option attributes [Array] :amount the amount to pay # @return [Stellar::Operation] the built operation, containing a # Stellar::PaymentOp body def payment(attributes = {}) destination = attributes[:destination] asset, amount = get_asset_amount(attributes[:amount]) raise ArgumentError unless destination.is_a?(KeyPair) op = PaymentOp.new op.asset = asset op.amount = amount op.destination = destination.muxed_account make(attributes.merge({ body: [:payment, op] })) end # # Helper method to create a valid PathPaymentStrictReceiveOp, wrapped # in the necessary XDR structs to be included within a # transactions `operations` array. # # @deprecated Please use Operation.path_payment_strict_receive # # @see Stellar::Asset # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :destination the receiver of the payment # @option attributes [Array] :amount the destination asset and the amount to pay # @option attributes [Array] :with the source asset and maximum allowed source amount to pay with # @option attributes [Array] :path the payment path to use # # @return [Stellar::Operation] the built operation, containing a Stellar::PaymentOp body # def path_payment(attributes = {}) path_payment_strict_receive(attributes) end # # Helper method to create a valid PathPaymentStrictReceiveOp, wrapped # in the necessary XDR structs to be included within a # transactions `operations` array. # # @see Stellar::Asset # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :destination the receiver of the payment # @option attributes [Array] :amount the destination asset and the amount to pay # @option attributes [Array] :with the source asset and maximum allowed source amount to pay with # @option attributes [Array] :path the payment path to use # # @return [Stellar::Operation] the built operation, containing a Stellar::PaymentOp body # def path_payment_strict_receive(attributes = {}) destination = attributes[:destination] asset, amount = get_asset_amount(attributes[:amount]) send_asset, send_max = get_asset_amount(attributes[:with]) path = (attributes[:path] || []).map { |p| p.is_a?(Array) ? Stellar::Asset.send(*p) : p } raise ArgumentError unless destination.is_a?(KeyPair) op = PathPaymentStrictReceiveOp.new op.send_asset = send_asset op.send_max = send_max op.destination = destination.muxed_account op.dest_asset = asset op.dest_amount = amount op.path = path make(attributes.merge({ body: [:path_payment_strict_receive, op] })) end # # Helper method to create a valid PathPaymentStrictSendOp, wrapped # in the necessary XDR structs to be included within a # transactions `operations` array. # # @see Stellar::Asset # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :destination the receiver of the payment # @option attributes [Array] :amount the destination asset and the minimum amount of destination asset to be received # @option attributes [Array] :with the source asset and amount to pay with # @option attributes [Array] :path the payment path to use # # @return [Stellar::Operation] the built operation, containing a Stellar::PaymentOp body # def path_payment_strict_send(attributes = {}) destination = attributes[:destination] asset, dest_min = get_asset_amount(attributes[:amount]) send_asset, send_amount = get_asset_amount(attributes[:with]) path = (attributes[:path] || []).map { |p| p.is_a?(Array) ? Stellar::Asset.send(*p) : p } raise ArgumentError unless destination.is_a?(KeyPair) op = PathPaymentStrictSendOp.new op.send_asset = send_asset op.send_amount = send_amount op.destination = destination.muxed_account op.dest_asset = asset op.dest_min = dest_min op.path = path make(attributes.merge({ body: [:path_payment_strict_send, op] })) end def create_account(attributes = {}) destination = attributes[:destination] starting_balance = interpret_amount(attributes[:starting_balance]) raise ArgumentError unless destination.is_a?(KeyPair) op = CreateAccountOp.new op.destination = destination.account_id op.starting_balance = starting_balance make(attributes.merge({ body: [:create_account, op] })) end # Helper method to create a valid ChangeTrustOp, wrapped # in the necessary XDR structs to be included within a # transactions `operations` array. # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::Asset] :line the asset to trust # @option attributes [Fixnum] :limit the maximum amount to trust, defaults to max int64, # if the limit is set to 0 it deletes the trustline. # # @return [Stellar::Operation] the built operation, containing a # Stellar::ChangeTrustOp body def change_trust(attributes = {}) line = attributes[:line] unless line.is_a?(Asset) unless Asset::TYPES.include?(line[0]) fail ArgumentError, "must be one of #{Asset::TYPES}" end line = Asset.send(*line) end limit = attributes.key?(:limit) ? interpret_amount(attributes[:limit]) : MAX_INT64 raise ArgumentError, "Bad :limit #{limit}" unless limit.is_a?(Integer) op = ChangeTrustOp.new(line: line, limit: limit) make(attributes.merge({ body: [:change_trust, op] })) end # Helper method to create a valid CreateClaimableBalanceOp, ready to be used # within a transactions `operations` array. # # @see Stellar::DSL::Claimant # @see https://github.com/astroband/ruby-stellar-sdk/tree/master/base/examples/claimable_balances.rb # # @param asset [Asset] the asset to transfer to a claimable balance # @param amount [Fixnum] the amount of `asset` to put into a claimable balance # @param claimants [Array] accounts authorized to claim the balance in the future # # @return [Operation] the built operation def create_claimable_balance(asset:, amount:, claimants:, **attributes) op = CreateClaimableBalanceOp.new(asset: asset, amount: amount, claimants: claimants) make(attributes.merge(body: [:create_claimable_balance, op])) end # Helper method to create a valid CreateClaimableBalanceOp, ready to be used # within a transactions `operations` array. # # @see Stellar::DSL::Claimant # @see https://github.com/astroband/ruby-stellar-sdk/tree/master/base/examples/claimable_balances.rb # # @param balance_id [ClaimableBalanceID] unique ID of claimable balance # # @return [Operation] the built operation, containing a Stellar::ChangeTrustOp body def claim_claimable_balance(balance_id:, **attributes) op = ClaimClaimableBalanceOp.new(balance_id: balance_id) make(attributes.merge(body: [:claim_claimable_balance, op])) end def begin_sponsoring_future_reserves(sponsored:, **attributes) op = BeginSponsoringFutureReservesOp.new( sponsored_id: Stellar.KeyPair(sponsored).account_id ) make(attributes.merge(body: [:begin_sponsoring_future_reserves, op])) end def end_sponsoring_future_reserves(**attributes) make(attributes.merge(body: [:end_sponsoring_future_reserves])) end # @param sponsored [#to_keypair] owner of sponsored entry def revoke_sponsorship(sponsored:, **attributes) key_fields = attributes.slice(:offer_id, :data_name, :balance_id, :asset, :signer) raise ArgumentError, "conflicting attributes: #{key_fields.keys.join(", ")}" if key_fields.size > 1 account_id = Stellar.KeyPair(sponsored).account_id key, value = key_fields.first op = if key == :signer RevokeSponsorshipOp.signer(account_id: account_id, signer_key: Stellar.SignerKey(value)) else RevokeSponsorshipOp.ledger_key(LedgerKey.from(account_id: account_id, **key_fields)) end make(attributes.merge(body: [:revoke_sponsorship, op])) end def manage_sell_offer(attributes = {}) buying = attributes[:buying] if buying.is_a?(Array) buying = Asset.send(*buying) end selling = attributes[:selling] if selling.is_a?(Array) selling = Asset.send(*selling) end amount = interpret_amount(attributes[:amount]) offer_id = attributes[:offer_id] || 0 price = interpret_price(attributes[:price]) op = ManageSellOfferOp.new({ buying: buying, selling: selling, amount: amount, price: price, offer_id: offer_id }) make(attributes.merge({ body: [:manage_sell_offer, op] })) end def manage_buy_offer(attributes = {}) buying = attributes[:buying] if buying.is_a?(Array) buying = Asset.send(*buying) end selling = attributes[:selling] if selling.is_a?(Array) selling = Asset.send(*selling) end amount = interpret_amount(attributes[:amount]) offer_id = attributes[:offer_id] || 0 price = interpret_price(attributes[:price]) op = ManageBuyOfferOp.new({ buying: buying, selling: selling, amount: amount, price: price, offer_id: offer_id }) make(attributes.merge({ body: [:manage_buy_offer, op] })) end def create_passive_sell_offer(attributes = {}) buying = attributes[:buying] if buying.is_a?(Array) buying = Asset.send(*buying) end selling = attributes[:selling] if selling.is_a?(Array) selling = Asset.send(*selling) end amount = interpret_amount(attributes[:amount]) price = interpret_price(attributes[:price]) op = CreatePassiveSellOfferOp.new({ buying: buying, selling: selling, amount: amount, price: price }) make(attributes.merge({ body: [:create_passive_sell_offer, op] })) end # # Helper method to create a valid SetOptionsOp, wrapped # in the necessary XDR structs to be included within a # transactions `operations` array. # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :inflation_dest # @option attributes [Array] :set flags to set # @option attributes [Array] :clear flags to clear # @option attributes [String] :thresholds # @option attributes [Stellar::Signer] :signer # # @return [Stellar::Operation] the built operation, containing a # Stellar::SetOptionsOp body def set_options(attributes = {}) op = SetOptionsOp.new op.set_flags = Stellar::AccountFlags.make_mask attributes[:set] op.clear_flags = Stellar::AccountFlags.make_mask attributes[:clear] op.master_weight = attributes[:master_weight] op.low_threshold = attributes[:low_threshold] op.med_threshold = attributes[:med_threshold] op.high_threshold = attributes[:high_threshold] op.signer = attributes[:signer] op.home_domain = attributes[:home_domain] inflation_dest = attributes[:inflation_dest] if inflation_dest raise ArgumentError, "Bad :inflation_dest" unless inflation_dest.is_a?(Stellar::KeyPair) op.inflation_dest = inflation_dest.account_id end make(attributes.merge({ body: [:set_options, op] })) end # # Helper method to create a valid AllowTrustOp, wrapped # in the necessary XDR structs to be included within a # transactions `operations` array. # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :trustor # @option attributes [Stellar::Asset] :asset # @option attributes [Symbol, Boolean] :authorize :full, maintain_liabilities or :none # # @return [Stellar::Operation] the built operation, containing a # Stellar::AllowTrustOp body def allow_trust(attributes = {}) op = AllowTrustOp.new trustor = attributes[:trustor] authorize = attributes[:authorize] asset = attributes[:asset] if asset.is_a?(Array) asset = Asset.send(*asset) end raise ArgumentError, "Bad :trustor" unless trustor.is_a?(Stellar::KeyPair) op.authorize = case authorize when :none, false then 0 # we handle booleans here for the backward compatibility when :full, true then TrustLineFlags.authorized_flag.value when :maintain_liabilities then TrustLineFlags.authorized_to_maintain_liabilities_flag.value else raise ArgumentError, "Bad :authorize, supported values: :full, :maintain_liabilities, :none" end raise ArgumentError, "Bad :asset" unless asset.type == Stellar::AssetType.asset_type_credit_alphanum4 atc = AllowTrustOp::Asset.new(:asset_type_credit_alphanum4, asset.code) op.trustor = trustor.account_id op.asset = atc make(attributes.merge({ body: [:allow_trust, op] })) end # # Helper method to create an account merge operation # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Stellar::KeyPair] :destination # # @return [Stellar::Operation] the built operation def account_merge(attributes = {}) destination = attributes[:destination] raise ArgumentError, "Bad :destination" unless destination.is_a?(KeyPair) # TODO: add source_account support make(attributes.merge({ body: [:account_merge, destination.muxed_account] })) end # # Helper method to create an inflation operation # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Integer] :sequence # # @return [Stellar::Operation] the built operation def inflation(attributes = {}) sequence = attributes[:sequence] raise ArgumentError, "Bad :sequence #{sequence}" unless sequence.is_a?(Integer) # TODO: add source_account support make(attributes.merge({ body: [:inflation] })) end # # Helper method to create an manage data operation # # @param [Hash] attributes the attributes to create the operation with # @option attributes [Integer] :sequence # # @return [Stellar::Operation] the built operation def manage_data(attributes = {}) op = ManageDataOp.new name = attributes[:name] value = attributes[:value] raise ArgumentError, "Invalid :name" unless name.is_a?(String) raise ArgumentError, ":name too long" unless name.bytesize <= 64 if value.present? raise ArgumentError, ":value too long" unless value.bytesize <= 64 end op.data_name = name op.data_value = value make(attributes.merge({ body: [:manage_data, op] })) end def bump_sequence(attributes = {}) op = BumpSequenceOp.new bump_to = attributes[:bump_to] raise ArgumentError, ":bump_to too big" unless bump_to <= MAX_INT64 op.bump_to = bump_to make(attributes.merge({ body: [:bump_sequence, op] })) end private def get_asset_amount(values) amount = interpret_amount(values.last) asset = if values[0].is_a?(Stellar::Asset) values.first else Stellar::Asset.send(*values[0...-1]) end [asset, amount] end def interpret_amount(amount) case amount when String (BigDecimal(amount) * Stellar::ONE).floor when Integer amount * Stellar::ONE when Numeric (amount * Stellar::ONE).floor else raise ArgumentError, "Invalid amount type: #{amount.class}. Must be String or Numeric" end end def interpret_price(price) case price when String bd = BigDecimal(price) Price.from_f(bd) when Numeric Price.from_f(price) when Stellar::Price price else raise ArgumentError, "Invalid price type: #{price.class}. Must be String, Numeric, or Stellar::Price" end end end end end