Skip to content

Commit

Permalink
feat: Support async faucet transactions
Browse files Browse the repository at this point in the history
This adds support for asynchronous faucet transactions. This will
make faucet transactions consistent with other transactions and will
make them more stable, as we can poll for the status on the SDK side
rather than on the server side.

```
faucet_tx = wallet.faucet
faucet_tx.wait!
```

or

```
wallet.faucet.wait!
```
  • Loading branch information
alex-stone committed Oct 29, 2024
1 parent 86889b6 commit b61c3df
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 23 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ testnet ETH. You are allowed one faucet claim per 24-hour window.
# Fund the wallet with a faucet transaction.
faucet_tx = wallet1.faucet

# Wait for the faucet transaction to complete.
faucet_tx.wait!

puts "Faucet transaction successfully completed: #{faucet_tx}"
```

Expand Down
11 changes: 8 additions & 3 deletions lib/coinbase/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,16 @@ def transactions
# @raise [Coinbase::FaucetLimitReachedError] If the faucet limit has been reached for the address or user.
# @raise [Coinbase::Client::ApiError] If an unexpected error occurs while requesting faucet funds.
def faucet(asset_id: nil)
opts = { asset_id: asset_id }.compact

Coinbase.call_api do
Coinbase::FaucetTransaction.new(
addresses_api.request_external_faucet_funds(network.normalized_id, id, opts)
addresses_api.request_external_faucet_funds(
network.normalized_id,
id,
{
asset_id: asset_id,
skip_wait: true
}.compact
)
)
end
end
Expand Down
68 changes: 64 additions & 4 deletions lib/coinbase/faucet_transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,80 @@ def initialize(model)
@model = model
end

# Returns the Faucet transaction.
# @return [Coinbase::Transaction] The Faucet transaction
def transaction
@transaction ||= Coinbase::Transaction.new(@model.transaction)
end

# Returns the status of the Faucet transaction.
# @return [Symbol] The status
def status
transaction.status
end

# Returns the transaction hash.
# @return [String] The onchain transaction hash
def transaction_hash
model.transaction_hash
transaction.transaction_hash
end

# Returns the link to the transaction on the blockchain explorer.
# @return [String] The link to the transaction on the blockchain explorer
def transaction_link
model.transaction_link
transaction.transaction_link
end

# Returns the Network of the Transaction.
# @return [Coinbase::Network] The Network
def network
transaction.network
end

# Waits until the FaucetTransaction is completed or failed by polling on the given interval.
# @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
# @raise [Timeout::Error] if the FaucetTransaction takes longer than the given timeout
# @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 transaction.terminal_state?

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

self.sleep interval_seconds
end

self
end

def reload
@model = Coinbase.call_api do
addresses_api.get_faucet_transaction(
network.normalized_id,
transaction.to_address_id,
transaction_hash
)
end

@transaction = Coinbase::Transaction.new(@model.transaction)

self
end

# Returns a String representation of the FaucetTransaction.
# @return [String] a String representation of the FaucetTransaction
def to_s
"Coinbase::FaucetTransaction{transaction_hash: '#{transaction_hash}', transaction_link: '#{transaction_link}'}"
Coinbase.pretty_print_object(
self.class,
status: transaction.status,
transaction_hash: transaction_hash,
transaction_link: transaction_link
)
end

# Same as to_s.
Expand All @@ -38,6 +96,8 @@ def inspect

private

attr_reader :model
def addresses_api
@addresses_api ||= Coinbase::Client::ExternalAddressesApi.new(Coinbase.configuration.api_client)
end
end
end
6 changes: 6 additions & 0 deletions lib/coinbase/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def initialize(model)
@model = model
end

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

# Returns the Unsigned Payload of the Transaction.
# @return [String] The Unsigned Payload
def unsigned_payload
Expand Down
40 changes: 40 additions & 0 deletions spec/factories/faucet_transaction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

FactoryBot.define do
factory :faucet_tx_model, class: Coinbase::Client::FaucetTransaction do
transient do
status { 'broadcasted' }
network_trait { :base_sepolia }
to_address_id { nil }
transaction_hash { nil }
end

# Default traits
base_sepolia
pending

TX_TRAITS.each do |status|
trait status do
status { status }
end
end

NETWORK_TRAITS.each do |network|
trait network do
network_trait { network }
end
end

after(:build) do |transfer, transients|
transfer.transaction = build(
:transaction_model,
transients.status,
transients.network_trait,
{
to_address_id: transients.to_address_id,
transaction_hash: transients.transaction_hash
}.compact
)
end
end
end
7 changes: 7 additions & 0 deletions spec/factories/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@

# Default trait.
pending
base_sepolia

NETWORK_TRAITS.each do |network|
trait network do
network_id { Coinbase.normalize_network(network) }
end
end

trait :pending do
status { 'pending' }
Expand Down
35 changes: 29 additions & 6 deletions spec/support/shared_examples/address_balances.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,18 @@
end

describe '#faucet' do
let(:tx_hash) { '0xdeadbeef' }
let(:faucet_tx) do
instance_double(Coinbase::Client::FaucetTransaction, transaction_hash: tx_hash)
build(:faucet_tx_model, network_id, :broadcasted, to_address_id: address_id)
end
let(:tx_hash) { faucet_tx.transaction.transaction_hash }

context 'when the request is successful' do
subject(:faucet_response) { address.faucet }

before do
allow(external_addresses_api)
.to receive(:request_external_faucet_funds)
.with(normalized_network_id, address_id, {})
.with(normalized_network_id, address_id, { skip_wait: true })
.and_return(faucet_tx)
end

Expand All @@ -161,7 +161,7 @@

expect(external_addresses_api)
.to have_received(:request_external_faucet_funds)
.with(normalized_network_id, address_id, {})
.with(normalized_network_id, address_id, { skip_wait: true })
end

it 'returns the faucet transaction' do
Expand All @@ -173,11 +173,34 @@
end
end

context 'when the request is unsuccesful' do
context 'when using specified asset' do
subject(:faucet_response) { address.faucet(asset_id: :usdc) }

before do
allow(external_addresses_api)
.to receive(:request_external_faucet_funds)
.with(normalized_network_id, address_id, { asset_id: :usdc, skip_wait: true })
.and_return(faucet_tx)
end

it 'requests external faucet funds for the address for the specified asset' do
faucet_response

expect(external_addresses_api)
.to have_received(:request_external_faucet_funds)
.with(normalized_network_id, address_id, { asset_id: :usdc, skip_wait: true })
end

it 'returns the faucet transaction' do
expect(faucet_response).to be_a(Coinbase::FaucetTransaction)
end
end

context 'when the request is unsuccessful' do
before do
allow(external_addresses_api)
.to receive(:request_external_faucet_funds)
.with(normalized_network_id, address_id, {})
.with(normalized_network_id, address_id, { skip_wait: true })
.and_raise(api_error)
end

Expand Down
105 changes: 101 additions & 4 deletions spec/unit/coinbase/faucet_transaction_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,134 @@
describe Coinbase::FaucetTransaction do
subject(:faucet_transaction) { described_class.new(model) }

let(:network_id) { :base_sepolia }
let(:transaction_hash) { '0x6c087c1676e8269dd81e0777244584d0cbfd39b6997b3477242a008fa9349e11' }
let(:transaction_link) { "https://sepolia.basescan.org/tx/#{transaction_hash}" }
let(:model) do
Coinbase::Client::FaucetTransaction.new(
let(:address_id) { Eth::Key.new.address.to_s }
let(:transaction_model) do
build(
:transaction_model,
:broadcasted,
network_id,
to_address_id: address_id,
transaction_hash: transaction_hash,
transaction_link: transaction_link
)
end
let(:model) do
Coinbase::Client::FaucetTransaction.new(transaction: transaction_model)
end

let(:external_addresses_api) { instance_double(Coinbase::Client::ExternalAddressesApi) }

before do
allow(Coinbase::Client::ExternalAddressesApi).to receive(:new).and_return(external_addresses_api)
end

describe '#initialize' do
it 'initializes a new FaucetTransaction' do
expect(faucet_transaction).to be_a(described_class)
end
end

describe '#transaction' do
it 'returns the transaction' do
expect(faucet_transaction.transaction).to be_a(Coinbase::Transaction)
end
end

describe '#transaction_hash' do
it 'returns the transaction hash' do
expect(faucet_transaction.transaction_hash).to eq(transaction_hash)
end
end

describe '#status' do
it 'returns the transaction status' do
expect(faucet_transaction.status).to eq(Coinbase::Transaction::Status::BROADCAST)
end
end

describe '#transaction_link' do
it 'returns the transaction link' do
expect(faucet_transaction.transaction_link).to eq(transaction_link)
end
end

describe '#network' do
it 'returns the network' do
expect(faucet_transaction.network).to be_a(Coinbase::Network)
end
end

describe '#reload' do
let(:updated_transaction_model) do
build(
:transaction_model,
:completed,
to_address_id: address_id,
transaction_hash: transaction_hash,
transaction_link: transaction_link
)
end
let(:updated_model) do
Coinbase::Client::FaucetTransaction.new(transaction: updated_transaction_model)
end

before do
allow(external_addresses_api)
.to receive(:get_faucet_transaction)
.with('base-sepolia', address_id, transaction_hash)
.and_return(updated_model)
end

it 'updates the faucet transaction' do
expect(faucet_transaction.reload.transaction.status).to eq(Coinbase::Transaction::Status::COMPLETE)
end
end

describe '#wait!' do
before do
allow(faucet_transaction).to receive(:sleep) # rubocop:disable RSpec/SubjectStub

allow(external_addresses_api)
.to receive(:get_faucet_transaction)
.with('base-sepolia', address_id, transaction_hash)
.and_return(model, model, updated_model)
end

context 'when the faucet transaction is completed' do
let(:updated_model) { build(:faucet_tx_model, network_id, :completed) }

it 'returns the completed FaucetTransaction' do
expect(faucet_transaction.wait!.status).to eq(Coinbase::Transaction::Status::COMPLETE)
end
end

context 'when the faucet transaction is failed' do
let(:updated_model) { build(:faucet_tx_model, network_id, :failed) }

it 'returns the failed FaucetTransaction' do
expect(faucet_transaction.wait!.status).to eq(Coinbase::Transaction::Status::FAILED)
end
end

context 'when the faucet transaction times out' do
let(:updated_model) { build(:faucet_tx_model, network_id, :broadcasted) }

it 'raises a Timeout::Error' do
expect { faucet_transaction.wait!(0.2, 0.00001) }.to raise_error(Timeout::Error, 'Faucet transaction timed out')
end
end
end

describe '#to_s' do
it 'returns a string representation of the FaucetTransaction' do
expect(faucet_transaction.to_s).to eq(
"Coinbase::FaucetTransaction{transaction_hash: '#{transaction_hash}', transaction_link: '#{transaction_link}'}"
expect(faucet_transaction.to_s).to include(
'Coinbase::FaucetTransaction',
transaction_hash,
transaction_link,
'broadcast'
)
end
end
Expand Down
Loading

0 comments on commit b61c3df

Please sign in to comment.