Skip to content

Commit

Permalink
feat: Support funding wallets
Browse files Browse the repository at this point in the history
This adds support for funding wallets using `wallet.fund` or
`address.fund`.

This will be extended to support leveraging a fund quote in a
subsequent commit.
  • Loading branch information
alex-stone committed Nov 18, 2024
1 parent 645b90d commit 2b2ec63
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/coinbase.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
require_relative 'coinbase/faucet_transaction'
require_relative 'coinbase/middleware'
require_relative 'coinbase/network'
require_relative 'coinbase/fund_operation'
require_relative 'coinbase/fund_quote'
require_relative 'coinbase/pagination'
require_relative 'coinbase/payload_signature'
Expand Down
14 changes: 14 additions & 0 deletions lib/coinbase/address/wallet_address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ def sign_payload(unsigned_payload:)
)
end

# Funds the address from your account on the Coinbase Platform for the given amount of the given Asset.
# @param amount [Integer, Float, BigDecimal] The amount of the Asset to fund the wallet with.
# @param asset_id [Symbol] The ID of the Asset to trade from. For Ether, :eth, :gwei, and :wei are supported.
# @return [Coinbase::FundOperation] The FundOperation object.
def fund(amount, asset_id)
FundOperation.create(
address_id: id,
amount: amount,
asset_id: asset_id,
network: network,
wallet_id: wallet_id
)
end

# Gets a quote for a fund operation to fund the address from your Coinbase platform,
# account for the amount of the specified Asset.
# @param asset_id [Symbol] The ID of the Asset to trade from. For Ether, :eth, :gwei, and :wei are supported.
Expand Down
208 changes: 208 additions & 0 deletions lib/coinbase/fund_operation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# frozen_string_literal: true

require_relative 'constants'
require 'bigdecimal'
require 'eth'

module Coinbase
# A representation of a Fund Operation, which buys funds from the Coinbase platform,
# and sends then to the developer's address.
class FundOperation
# A representation of a Fund Operation status.
module Status
# The Fund Operation is being processed.
PENDING = 'pending'

# The Fund Operation is complete.
COMPLETE = 'complete'

# The Fund Operation has failed for some reason.
FAILED = 'failed'

# The states that are considered terminal on-chain.
TERMINAL_STATES = [COMPLETE, FAILED].freeze
end

class << self
# Creates a new Fund Operation object.
# @param address_id [String] The Address ID of the sending Address
# @param wallet_id [String] The Wallet ID of the sending Wallet
# @param amount [BigDecimal] The amount of the Asset to send
# @param network [Coinbase::Network, Symbol] The Network or Network ID of the Asset
# @param asset_id [Symbol] The Asset ID of the Asset to send
# @return [FundOperation] The new pending FundOperation object
# @raise [Coinbase::ApiError] If the FundOperation fails
def create(wallet_id:, address_id:, amount:, asset_id:, network:)
network = Coinbase::Network.from_id(network)
asset = network.get_asset(asset_id)

model = Coinbase.call_api do
fund_api.create_fund_operation(
wallet_id,
address_id,
{
amount: asset.to_atomic_amount(amount).to_i.to_s,
asset_id: asset.primary_denomination.to_s,
}
)
end

new(model)
end

# Enumerates the fund operation for a given address belonging to a wallet.
# The result is an enumerator that lazily fetches from the server, and can be iterated over,
# converted to an array, etc...
# @return [Enumerable<Coinbase::FundOperation>] Enumerator that returns fund operations
def list(wallet_id:, address_id:)
Coinbase::Pagination.enumerate(
->(page) { fetch_page(wallet_id, address_id, page) }
) do |fund_operation|
new(fund_operation)
end
end

private

def fund_api
Coinbase::Client::FundApi.new(Coinbase.configuration.api_client)
end

def fetch_page(wallet_id, address_id, page)
fund_api.list_fund_operations(
wallet_id,
address_id,
limit: DEFAULT_PAGE_LIMIT,
page: page
)
end
end

# Returns a new FundOperation object. Do not use this method directly. Instead, use
# Wallet#fund or Address#fund.
# @param model [Coinbase::Client::FundOperation] The underlying FundOperation object
def initialize(model)
raise unless model.is_a?(Coinbase::Client::FundOperation)

@model = model
end

# Returns the FundOperation ID.
# @return [String] The FundOperation ID
def id
@model.fund_operation_id
end

# Returns the Network of the FundOperation.
# @return [Coinbase::Network] The Network
def network
@network ||= Coinbase::Network.from_id(@model.network_id)
end

# Returns the Wallet ID of the FundOperation.
# @return [String] The Wallet ID
def wallet_id
@model.wallet_id
end

# Returns the From Address ID of the FundOperation.
# @return [String] The From Address ID
def address_id
@model.address_id
end

# Returns the Asset of the FundOperation.
# @return [Coinbase::Asset] The Asset
def asset
@asset ||= Coinbase::Asset.from_model(@model.crypto_amount.asset)
end

# Returns the amount of the asset for the Transfer.
# @return [BigDecimal] The amount of the asset
def amount
BigDecimal(@model.crypto_amount.amount) / BigDecimal(10).power(@model.crypto_amount.asset.decimals)
end

# Returns the amount of Fiat the FundOperation will cost (inclusive of fees).
# @return [BigDecimal] The amount of Fiat
def fiat_amount
BigDecimal(@model.fiat_amount.amount)
end

# Returns the Fiat currency of the FundOperation.
# @return [String] The Fiat currency
def fiat_currency
@model.fiat_amount.currency
end

# Returns the status of the Fund Operation.
# @return [Symbol] The status
def status
@model.status
end

# Reload reloads the Transfer model with the latest version from the server side.
# @return [Transfer] The most recent version of Transfer from the server.
def reload
@model = Coinbase.call_api do
fund_api.get_fund_operation(wallet_id, address_id, id)
end

self
end

# Waits until the Transfer is completed or failed by polling the Network at the given interval. Raises a
# Timeout::Error if the Transfer takes longer than the given timeout.
# @param interval_seconds [Integer] The interval at which to poll the Network, in seconds
# @param timeout_seconds [Integer] The maximum amount of time to wait for the Transfer to complete, in seconds
# @return [Transfer] The completed Transfer object
def wait!(interval_seconds = 0.2, timeout_seconds = 20)
start_time = Time.now

loop do
reload

return self if terminal_state?

raise Timeout::Error, 'Transfer timed out' if Time.now - start_time > timeout_seconds

self.sleep interval_seconds
end

self
end

# Returns a String representation of the Fund Operation.
# @return [String] a String representation of the Fund Operation
def to_s
Coinbase.pretty_print_object(
self.class,
id: id,
network_id: network.id,
wallet_id: wallet_id,
address_id: address_id,
amount: amount,
asset_id: asset.asset_id,
status: status
)
end

# Same as to_s.
# @return [String] a String representation of the Transfer
def inspect
to_s
end

private

# Returns whether the Fund Operation is in a terminal state.
# @return [Boolean] Whether the Fund Operation is in a terminal state
def terminal_state?
Status::TERMINAL_STATES.include?(status)
end

def fund_api
@fund_api ||= Coinbase::Client::FundApi.new(Coinbase.configuration.api_client)
end
end
end
6 changes: 5 additions & 1 deletion lib/coinbase/fund_quote.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,14 @@ def amount
CryptoAmount.from_model(@model.crypto_amount)
end

# Returns the amount of Fiat the FundOperation will cost (inclusive of fees).
# @return [BigDecimal] The amount of Fiat
def fiat_amount
@model.fiat_amount.amount
BigDecimal(@model.fiat_amount.amount)
end

# Returns the Fiat currency of the FundOperation.
# @return [String] The Fiat currency
def fiat_currency
@model.fiat_amount.currency
end
Expand Down
8 changes: 7 additions & 1 deletion lib/coinbase/wallet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,15 @@ def initialize(model, seed: nil)
# @param asset_id [Symbol] The ID of the Asset to trade from. For Ether, :eth, :gwei, and :wei are supported.
# @return [Coinbase::FundQuote] The FundQuote object.

# @!method fund
# Funds the address from your account on the Coinbase Platform for the given amount of the given Asset.
# @param amount [Integer, Float, BigDecimal] The amount of the Asset to fund the wallet with.
# @param asset_id [Symbol] The ID of the Asset to trade from. For Ether, :eth, :gwei, and :wei are supported.
# @return [Coinbase::FundOperation] The FundOperation object.

def_delegators :default_address, :transfer, :trade, :faucet, :stake, :unstake, :claim_stake, :staking_balances,
:stakeable_balance, :unstakeable_balance, :claimable_balance, :sign_payload, :invoke_contract,
:deploy_token, :deploy_nft, :deploy_multi_token, :quote_fund
:deploy_token, :deploy_nft, :deploy_multi_token, :quote_fund, :fund

# Returns the addresses belonging to the Wallet.
# @return [Array<Coinbase::WalletAddress>] The addresses belonging to the Wallet
Expand Down

0 comments on commit 2b2ec63

Please sign in to comment.