require "bigdecimal" module Stellar class Operation MAX_INT64 = 2**63 - 1 TRUST_LINE_FLAGS_MAPPING = { full: Stellar::TrustLineFlags.authorized_flag, maintain_liabilities: Stellar::TrustLineFlags.authorized_to_maintain_liabilities_flag, clawback_enabled: Stellar::TrustLineFlags.trustline_clawback_enabled_flag }.freeze class << self include Stellar::DSL # # Construct a new Stellar::Operation from the provided # source account and body # # @param source_account [KeyPair, nil] the source account for the operation # @param [(Symbol, XDR::Struct)] body a tuple containing operation type and operation object # # @return [Stellar::Operation] the built operation def make(body:, source_account: nil) raise ArgumentError, "Bad :source_account" if source_account && !source_account.is_a?(Stellar::KeyPair) body = Stellar::Operation::Body.new(*body) Stellar::Operation.new( source_account: source_account&.muxed_account, body: body ) end # Create Account operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param destination [KeyPair] the account to create # @param starting_balance [String, Numeric] the amount to deposit to the newly created account # # @return [Stellar::Operation] the built operation def create_account(destination:, starting_balance:, source_account: nil) op = CreateAccountOp.new( destination: KeyPair(destination).account_id, starting_balance: interpret_amount(starting_balance) ) make(source_account: source_account, body: [:create_account, op]) end # Account Merge operation builder # # @param [Stellar::KeyPair, nil] source_account the source account for the operation # @param [Stellar::KeyPair] destination the account to merge into # # @return [Stellar::Operation] the built operation def account_merge(destination:, source_account: nil) raise ArgumentError, "Bad destination" unless destination.is_a?(KeyPair) make(source_account: source_account, body: [:account_merge, destination.muxed_account]) end # Set Options operation builder. # # @param source_account [KeyPair, nil] the source account for the operation # @param home_domain [String, nil\] the home domain of the account # @param signer [Signer, nil] add, remove or adjust weight of the co-signer # @param set [Array] flags to set # @param clear [Array] flags to clear # @param inflation_dest [KeyPair, nil] the inflation destination of the account # # @return [Stellar::Operation] the built operation def set_options(set: [], clear: [], home_domain: nil, signer: nil, inflation_dest: nil, source_account: nil, **attributes) raise ArgumentError, "Bad inflation_dest" if inflation_dest && !inflation_dest.is_a?(KeyPair) op = SetOptionsOp.new( set_flags: Stellar::AccountFlags.make_mask(set), clear_flags: Stellar::AccountFlags.make_mask(clear), master_weight: attributes[:master_weight], low_threshold: attributes[:low_threshold], med_threshold: attributes[:med_threshold], high_threshold: attributes[:high_threshold], signer: signer, home_domain: home_domain, inflation_dest: inflation_dest&.account_id ) make(source_account: source_account, body: [:set_options, op]) end # Bump Sequence operation builder # # @param [Stellar::KeyPair] source_account the source account for the operation # @param [Integer] bump_to the target sequence number for the account # # @return [Stellar::Operation] the built operation def bump_sequence(bump_to:, source_account: nil) raise ArgumentError, ":bump_to too big" unless bump_to <= MAX_INT64 op = BumpSequenceOp.new( bump_to: bump_to ) make(source_account: source_account, body: [:bump_sequence, op]) end # Manage Data operation builder # # @param [Stellar::KeyPair, nil] source_account the source account for the operation # @param [String] name the name of the data entry # @param [String, nil] value the value of the data entry (nil to remove the entry) # # @return [Stellar::Operation] the built operation def manage_data(name:, value: nil, source_account: nil) raise ArgumentError, "Invalid :name" unless name.is_a?(String) raise ArgumentError, ":name too long" if name.bytesize > 64 raise ArgumentError, ":value too long" if value && value.bytesize > 64 op = ManageDataOp.new( data_name: name, data_value: value ) make(source_account: source_account, body: [:manage_data, op]) end # Change Trust operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param asset [Asset] the asset to trust # @param limit [String, Numeric] the maximum amount to trust, defaults to max int64 (0 deletes the trustline) # # @return [Stellar::Operation] the built operation def change_trust(asset: nil, limit: nil, source_account: nil, **attrs) if attrs.key?(:line) && !asset Stellar::Deprecation.warn("`line` parameter is deprecated, use `asset` instead") asset = attrs[:line] end op = ChangeTrustOp.new( line: Asset(asset).to_change_trust_asset, limit: limit ? interpret_amount(limit) : MAX_INT64 ) make(source_account: source_account, body: [:change_trust, op]) end # Set Trustline Flags operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param asset [Stellar::Asset] # @param trustor [Stellar::KeyPair] # @param flags [{String, Symbol, Stellar::TrustLineFlags => true, false}] flags to to set or clear # # @return [Stellar::Operation] the built operation def set_trust_line_flags(asset:, trustor:, flags: {}, source_account: nil) op = Stellar::SetTrustLineFlagsOp.new( trustor: KeyPair(trustor).account_id, asset: Asset(asset), attributes: TrustLineFlags.set_clear_masks(flags) ) make(source_account: source_account, body: [:set_trust_line_flags, op]) end # Clawback operation builder # # @param [Stellar::KeyPair] source_account the source account for the operation # @param [String|Account|PublicKey|SignerKey|KeyPair] from the account to clawback from # @param [(Asset, Numeric)] amount the amount of asset to subtract from the balance # # @return [Stellar::Operation] the built operation def clawback(from:, amount:, source_account: nil) asset, amount = get_asset_amount(amount) if amount == 0 raise ArgumentError, "Amount can not be zero" end if amount < 0 raise ArgumentError, "Negative amount is not allowed" end op = ClawbackOp.new( amount: amount, from: KeyPair(from).muxed_account, asset: asset ) make(source_account: source_account, body: [:clawback, op]) end # Create Claimable Balance operation builder. # # @see Stellar::DSL::Claimant # @see https://github.com/astroband/ruby-stellar-sdk/tree/master/base/examples/claimable_balances.rb # # @param source_account [KeyPair, nil] the source account for the operation # @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:, source_account: nil) op = CreateClaimableBalanceOp.new(asset: asset, amount: amount, claimants: claimants) make(source_account: source_account, 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 source_account [KeyPair, nil] the source account for the operation # @param balance_id [ClaimableBalanceID] unique ID of claimable balance # # @return [Operation] the built operation def claim_claimable_balance(balance_id:, source_account: nil) op = ClaimClaimableBalanceOp.new(balance_id: balance_id) make(source_account: source_account, body: [:claim_claimable_balance, op]) end # Clawback Claimable Balance operation builder # # @param [Stellar::KeyPair] source_account the source account for the operation # @param [String] balance_id claimable balance ID as a hexadecimal string # # @return [Stellar::Operation] the built operation def clawback_claimable_balance(balance_id:, source_account: nil) balance_id = Stellar::ClaimableBalanceID.from_xdr(balance_id, :hex) op = ClawbackClaimableBalanceOp.new(balance_id: balance_id) make(source_account: source_account, body: [:clawback_claimable_balance, op]) rescue XDR::ReadError raise ArgumentError, "Claimable balance id '#{balance_id}' is invalid" end # Payment Operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param [Stellar::KeyPair] destination the receiver of the payment # @param [(Asset, Numeric)] amount the amount to pay # # @return [Stellar::Operation] the built operation def payment(destination:, amount:, source_account: nil) raise ArgumentError unless destination.is_a?(KeyPair) asset, amount = get_asset_amount(amount) op = PaymentOp.new( asset: asset, amount: amount, destination: destination.muxed_account ) make( source_account: source_account, body: [:payment, op] ) end # Path Payment Strict Receive operation builder. # # @param source_account [KeyPair, nil] the source account for the operation # @param destination [Stellar::KeyPair] the receiver of the payment # @param amount [Array] the destination asset and the amount to pay # @param with [Array] the source asset and maximum allowed source amount to pay with # @param path [Array] the payment path to use # # @return [Stellar::Operation] the built operation def path_payment_strict_receive(destination:, amount:, with:, path: [], source_account: nil) raise ArgumentError unless destination.is_a?(KeyPair) dest_asset, dest_amount = get_asset_amount(amount) send_asset, send_max = get_asset_amount(with) op = PathPaymentStrictReceiveOp.new( destination: destination.muxed_account, dest_asset: dest_asset, dest_amount: dest_amount, send_asset: send_asset, send_max: send_max, path: path.map { |p| Asset(p) } ) make(source_account: source_account, body: [:path_payment_strict_receive, op]) end alias_method :path_payment, :path_payment_strict_receive # Path Payment Strict Receive operation builder. # # @param source_account [KeyPair, nil] the source account for the operation # @param destination [Stellar::KeyPair] the receiver of the payment # @param amount [Array] the destination asset and the minimum amount of destination asset to be received # @param with [Array] the source asset and amount to pay with # @param path [Array] the payment path to use # # @return [Stellar::Operation] the built operation def path_payment_strict_send(destination:, amount:, with:, path: [], source_account: nil) raise ArgumentError unless destination.is_a?(KeyPair) dest_asset, dest_min = get_asset_amount(amount) send_asset, send_amount = get_asset_amount(with) op = PathPaymentStrictSendOp.new( destination: destination.muxed_account, send_asset: send_asset, send_amount: send_amount, dest_asset: dest_asset, dest_min: dest_min, path: path.map { |p| Asset(p) } ) make(source_account: source_account, body: [:path_payment_strict_send, op]) end # Manage Sell Offer operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param selling [Asset] the asset to sell # @param buying [Asset] the asset to buy # @param amount [String, Numeric] the amount of asset to sell # @param price [String, Numeric, Price] the price of the selling asset in terms of buying asset # @param offer_id [Integer] the offer ID to modify (0 to create a new offer) # # @return [Operation] the built operation def manage_sell_offer(selling:, buying:, amount:, price:, offer_id: 0, source_account: nil) selling = Asset.send(*selling) if selling.is_a?(Array) buying = Asset.send(*buying) if buying.is_a?(Array) op = ManageSellOfferOp.new( buying: buying, selling: selling, amount: interpret_amount(amount), price: Price.from(price), offer_id: offer_id ) make(source_account: source_account, body: [:manage_sell_offer, op]) end # Manage Buy Offer operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param buying [Asset] the asset to buy # @param selling [Asset] the asset to sell # @param amount [String, Numeric] the amount of asset to buy # @param price [String, Numeric, Price] the price of the buying asset in terms of the selling asset # @param offer_id [Integer] the offer ID to modify (0 to create a new offer) # # @return [Operation] the built operation def manage_buy_offer(buying:, selling:, amount:, price:, offer_id: 0, source_account: nil) buying = Asset.send(*buying) if buying.is_a?(Array) selling = Asset.send(*selling) if selling.is_a?(Array) op = ManageBuyOfferOp.new( buying: buying, selling: selling, buy_amount: interpret_amount(amount), price: Price.from(price), offer_id: offer_id ) make(source_account: source_account, body: [:manage_buy_offer, op]) end # Create Passive Sell Offer operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param selling [Asset] the asset to sell # @param buying [Asset] the asset to buy # @param amount [String, Numeric] the amount of asset to sell # @param price [String, Numeric, Price] the price of the selling asset in terms of buying asset # # @return [Operation] the built operation def create_passive_sell_offer(selling:, buying:, amount:, price:, source_account: nil) selling = Asset.send(*selling) if selling.is_a?(Array) buying = Asset.send(*buying) if buying.is_a?(Array) op = CreatePassiveSellOfferOp.new( buying: buying, selling: selling, amount: interpret_amount(amount), price: Price.from(price) ) make(source_account: source_account, body: [:create_passive_sell_offer, op]) end # Liquidity Pool Deposit operation builder # # @param [Stellar::KeyPair] source_account the source account for the operation # @param [String] liquidity_pool_id the liquidity pool id as hexadecimal string # @param [String, Numeric] max_amount_a the maximum amount of asset A to deposit # @param [String, Numeric] max_amount_b the maximum amount of asset B to deposit # @param [String, Numeric, Stellar::Price] min_price the minimum valid price of asset A in terms of asset B # @param [String, Numeric, Stellar::Price] max_price the maximum valid price of asset A in terms of asset B # # @return [Stellar::Operation] the built operation def liquidity_pool_deposit(liquidity_pool_id:, max_amount_a:, max_amount_b:, min_price:, max_price:, source_account: nil) op = LiquidityPoolDepositOp.new( liquidity_pool_id: PoolID.from_xdr(liquidity_pool_id, :hex), max_amount_a: interpret_amount(max_amount_a), max_amount_b: interpret_amount(max_amount_b), min_price: Price.from(min_price), max_price: Price.from(max_price) ) make(source_account: source_account, body: [:liquidity_pool_deposit, op]) rescue XDR::ReadError raise ArgumentError, "invalid liquidity pool ID '#{balance_id}'" end # Liquidity Pool Withdraw operation builder # # @param [Stellar::KeyPair] source_account the source account for the operation # @param [String] liquidity_pool_id the liquidity pool id as hexadecimal string # @param [String, Numeric] amount the number of pool shares to withdraw # @param [String, Numeric] min_amount_a the minimum amount of asset A to withdraw # @param [String, Numeric] min_amount_b the minimum amount of asset B to withdraw # # @return [Stellar::Operation] the built operation def liquidity_pool_withdraw(liquidity_pool_id:, amount:, min_amount_a:, min_amount_b:, source_account: nil) op = LiquidityPoolWithdrawOp.new( liquidity_pool_id: PoolID.from_xdr(liquidity_pool_id, :hex), amount: interpret_amount(amount), min_amount_a: interpret_amount(min_amount_a), min_amount_b: interpret_amount(min_amount_b) ) make(source_account: source_account, body: [:liquidity_pool_withdraw, op]) rescue XDR::ReadError raise ArgumentError, "invalid liquidity pool ID '#{balance_id}'" end # Begin Sponsoring Future Reserves operation builder # # @param source_account [KeyPair, nil] the source account for the operation # # @return [Operation] the built operation def begin_sponsoring_future_reserves(sponsored:, source_account: nil) op = BeginSponsoringFutureReservesOp.new( sponsored_id: KeyPair(sponsored).account_id ) make(source_account: source_account, body: [:begin_sponsoring_future_reserves, op]) end # End Sponsoring Future Reserves operation builder # # @param source_account [KeyPair, nil] the source account for the operation # # @return [Operation] the built operation def end_sponsoring_future_reserves(source_account: nil) make(source_account: source_account, body: [:end_sponsoring_future_reserves]) end # Revoke Sponsorship operation builder # # @param source_account [KeyPair, nil] the source account for the operation # @param sponsored [#to_keypair] owner of sponsored entry # # @return [Operation] the built operation def revoke_sponsorship(sponsored:, source_account: nil, **attributes) key_fields = attributes.slice(:offer_id, :data_name, :balance_id, :liquidity_pool_id, :asset, :signer) raise ArgumentError, "conflicting attributes: #{key_fields.keys.join(", ")}" if key_fields.size > 1 account_id = KeyPair(sponsored).account_id key, value = key_fields.first op = if key == :signer RevokeSponsorshipOp.signer(account_id: account_id, signer_key: SignerKey(value)) else RevokeSponsorshipOp.ledger_key(LedgerKey.from(account_id: account_id, **key_fields)) end make(source_account: source_account, body: [:revoke_sponsorship, op]) end # Inflation operation builder # # @param [Stellar::KeyPair, nil] source_account the source account for the operation # # @return [Stellar::Operation] the built operation def inflation(source_account: nil) make(source_account: source_account, body: [:inflation]) 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) if amount.is_a?(Float) (amount * Stellar::ONE).floor else (BigDecimal(amount) * Stellar::ONE).floor end end end end end