/blockfrost-crystal

A Crystal SDK for the Blockfrost.io API

Primary LanguageCrystalMIT LicenseMIT

GitHub GitHub tag (latest SemVer) GitHub Workflow Status

blockfrost-crystal


A Crystal SDK for the Blockfrost.io API.

Getting startedInstallationUsage


Getting started

To use this SDK, you first need to log in to blockfrost.io to create your project to retrieve your API key.


Installation

  1. Add the dependency to your shard.yml:
dependencies:
  blockfrost:
    github: blockfrost/blockfrost-crystal
  1. Run shards install

Usage

Every endpoint of the Blockfrost API is covered by this library. It's too much to list them all here, but below are a few examples of their usage.

Configuration

require "blockfrost"

Create an initializer to configure the global API key(s):

# e.g. config/blockfrost.cr
Blockfrost.configure do |config|
  config.cardano_api_key = ENV.fetch("BLOCKFROST_CARDANO_API_KEY")
  config.ipfs_api_key = ENV.fetch("BLOCKFROST_IPFS_API_KEY")
end

There are several configuration options available. Here's an overview with some added information:

Blockfrost.configure do |config|
  # an API KEY starting with "testnet", "preview", "preprod" or "mainnet"
  config.cardano_api_key = ENV.fetch("BLOCKFROST_CARDANO_API_KEY")

  # the api version of the Cardano enpoints (currently only "v0") 
  config.cardano_api_version = "v0"

  # an API KEY starting with "ipfs"
  config.ipfs_api_key = ENV.fetch("BLOCKFROST_IPFS_API_KEY")

  # the api version of the IPFS enpoints (currently only "v0") 
  config.ipfs_api_version = "v0"

  # Blockfrost::QueryOrder::ASC or Blockfrost::QueryOrder::DESC
  config.default_order = Blockfrost::QueryOrder::DESC

  # default count per page in collection endpoints (min: 1; max: 100; default: 100)
  config.default_count_per_page = 42

  # number of times to retry in concurrent requests (min: 0; max: 10; default: 5)
  config.retries_in_concurrent_requests = 5

  # sleep between retries in concurrent requests (in ms; min: 0; default: 500)
  config.sleep_between_retries_ms = 1000
end

To use one or more different configuration values locally in your code, use the temp_config method with a block:

# any code here will use the global configuration

Blockfrost.temp_config(cardano_api_key: "preprodAbC...xYz") do
  # this code will use the "preprodAbC...xYz" api key
end

# any code following here will use the global configuration again

Single resources

Get the latest block:

block = Blockfrost::Block.latest

Or a specific block:

block_hash = "4ea1ba291e8eef538635a53e59fddba7810d1679631cc3aed7c8e6c4091a516a"
block = Blockfrost::Block.get(block_hash)

Nested resources

To get the transaction ids of a loaded block:

block.tx_ids

The same can be done in one go as well:

Blockfrost::Block.tx_ids(block_hash)

This pattern is used throughout the library. There will always be a class method and a corresponding instance method for nested resources.

Some nested resources have an additional scope parameter, like the utxos of an asset of an address:

address = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq..."
asset = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
utxos = Blockfrost::Address.utxos_of_asset(address, asset)

Collections, ordering and pagination

Get all assets:

assets = Blockfrost::Asset.all

Almost all collection methods accept three arguments for ordering and pagination:

assets = Blockfrost::Asset.all(
  order: "desc",
  count: 20,
  page: 1
)

NOTE: The count parameter should be a value between 1 and 100. Lower or higher values will be coerced to fit within that range.

The order parameter is converted to an enum in the background, so the underlying enum values are also accepted:

assets = Blockfrost::Asset.all(
  order: Blockfrost::QueryOrder::DESC,
  count: 20,
  page: 1
)

NOTE: Using the enum values is considered the safer option. If a string with a typo is passed (e.g. "decs"), the default_order from the settings will be used, whereas passing an enum with a typo will fail to compile. It's a choice of security over convenience.

Some endpoints don't have an order parameter, like .previous/next on blocks:

block_height = 15243592
Blockfrost::Block.get(block_height).next(count: 5, page: 2)

The transactions method for addresses exceptionally accepts two additional arguments:

address = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq..."
Blockfrost::Address.transactions(
  address,
  order: "desc",
  count: 10,
  page: 1,
  from: "8929261",
  to: "9999269:10"
)

Ordering and count per page can also be configured with the following two settings:

Blockfrost.configure do |config|
  # Blockfrost::QueryOrder::ASC or Blockfrost::QueryOrder::DESC
  config.default_order = Blockfrost::QueryOrder::DESC

  # minimum 1 and maximum 100
  config.default_count_per_page = 42
end

Concurrency for large collections

Every method accepting pagination parameters will also have a method overload accepting a pages : Range argument instead of page : Int32:

assets = Blockfrost::Asset.all(pages: 1..10)
assets.size
# => 1000

In the background, this method will make concurrent requests fetching 100 records for every single page number in the range. Then those results are concatenated into one big array and returned as the result.

Except for count and page, all other arguments are also still accepted. So the results can also be ordered:

assets = Blockfrost::Asset.all(1..10, "asc")

Or with nested resources:

pool_id = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
events = Blockfrost::Pool.history(pool_id, pages: 1..5)

It also handles all possible exceptions of the Blockfrost API. If your account is temporarily rate-limited (Blockfrost::Client::OverLimitException), it will retry 10 times and raise anyway after that. All other exceptions will be raised immediately.

There are two settings related to rate-limiting:

Blockfrost.configure do |config|
  # minimum 0, maximum 10; defaults to 10
  config.retries_in_concurrent_requests = 5

  # minimum 0, no maximum; defaults to 500
  config.validate_sleep_between_retries_ms = 1000
end

Here's how fetching sequentially vs concurrently compares in terms of loading times:

Description Elapsed
1 page 100 assets 187ms
10 pages sequentially 1000 assets 1s 873ms
10 pages concurrently 1000 assets 265ms
100 pages concurrently 10000 assets 427ms

(tested on a 1 Gb home connection from Spain)

Post endpoints

Submit an already serialized transaction to the network:

tx_data = File.open("path/to/cbor_data")
Blockfrost::Transaction.submit(tx_data)
# => "d1662b24fa9fe985fc2dce47455df399cb2e31e1e1819339e885801cc3578908"

IPFS endpoints

Add an object to IPFS:

object = Blockfrost::IPFS.add("path/to/file")

Pin an object to local storage:

result = object.pin
result.state
# => Blockfrost::IPFS::Pin::State::Queued

Or alternatively (and the same):

Blockfrost::IPFS::Pin.add(object.ipfs_hash)

To get all pinned objects:

Blockfrost::IPFS::Pin.all

Finally, to remove a pin:

Blockfrost::IPFS::Pin.remove(ipfs_hash)

As expected the instance method is also available on a pin:

pin = Blockfrost::IPFS::Pin.get(ipfs_hash)
result = pin.remove
result.state
# => Blockfrost::IPFS::Pin::State::Unpinned

Network selection

The Cardano network is selected based on the API key. If the configured API key starts with preprod..., then the preprod network will be used.

There are a few helper methods available to verify which network is selected. For example, to get the current network:

Blockfrost.configure do |config|
  config.cardano_api_key = "preprodsSDBoik1wn1NxxhB8GB0Bcv7LuarFAKE"
end

Blockfrost.cardano_network
# => "preprod"

Additionally, there are also methods to test against the current network:

Blockfrost.cardano_mainnet?
# => false
Blockfrost.cardano_preprod?
# => true

Blockfrost.temp_config(cardano_api_key: "mainnetsSDBoik1wn1NxxhB8GB0Bcv7LuarFAKE") do
  Blockfrost.cardano_mainnet?
  # => true
end

Blockfrost.cardano_mainnet?
# => false

Exception handling

All exceptions from the Blockfrost API can be caught with:

begin
  # do something
rescue e : Blockfrost::RequestException
  puts e.message
end

Or with more specificity:

begin
  # do something
rescue e : Blockfrost::Client::BadRequestException
  puts "Bad request (400)"
rescue e : Blockfrost::Client::ForbiddenException
  puts "Authentication secret is missing or invalid (403)"
rescue e : Blockfrost::Client::NotFoundException
  puts "Component not found (404)"
rescue e : Blockfrost::Client::IpBannedException
  puts "IP has been auto-banned for extensive sending of requests after usage limit has been reached (418)"
rescue e : Blockfrost::Client::OverLimitException
  puts "Usage limit reached (429)"
rescue e : Blockfrost::Client::ServerErrorException
  puts "Internal Server Error (500)"
end

Documentation

Development

Make sure you have Guardian.cr installed. Then run:

$ guardian

This will automatically:

  • run ameba for src and spec files
  • run the relevant spec for any file in the src dir
  • run a spec file whenever they are saved
  • install shards whenever you save shard.yml

Contributing

  1. Fork it (https://github.com/blockfrost/blockfrost-crystal/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Test your changes (crystal spec, crystal tool format and bin/ameba)
  4. Commit your changes (git commit -am 'feat: something new')
  5. Push to the branch (git push origin my-new-feature)
  6. Create a new Pull Request

NOTE: Please have a look at conventional commits for commit messages.

Contributors

  • Wout - creator and maintainer