- Python 3.9+
- Poetry 1.4+
A default configuration is hard-coded in the settings
module.
Configuration variables can then be overridden via a configuration file in
either of two locations. The first option is a config.py
file inside the app
instance
directory. The second option is a location provided via the
environment variable FAUCET_SETTINGS
. The two options can be combined, the
instance configuration will override the default one and the file pointed to by
the environment variable will take precedence, overriding both.
For the service to work it is necessary to configure at least the following variables:
MNEMONIC
: the mnemonic for the walletXPUB
: the extended public key of the walletNAME
: the name of the faucetASSETS
: the dictionary of asset groups to be used by the faucet
The ASSETS
variable is a dictionary with group names (strings) as keys. Each
group is a dictionary with the following fields:
label
(string): a label for the groupdistribution
(dictionary):mode
(int): 1 for standard, 2 for randomrandom_params
(dictionary): only required for random moderequest_window_open
: date and time for the opening of the request windowrequest_window_close
: date and time for the closing of the request window
assets
(list): a list of dictionaries, with each entry having the following items:asset_id
(string): the ID of the assetamount
(int): the amount to be sent to each recipient
Note: request window open/close don't include a UTC offset, so they should represent UTC time.
Standard distribution mode collects requests as pending and periodically serves them in batches.
Random distribution collects requests inside a request window (requests are otherwise not allowed) as waiting and, once the request window closes, selects a number of them (equal to the available assets) at random and sets them as pending (in order to be served) while the remaining ones are set as unmet (and will never be served).
An example ASSETS
declaration:
ASSETS = {
'group_1': {
'label': 'asset group one',
'distribution': {
'mode': 1,
},
'assets': [
{
'asset_id': 'rgb1aaa...',
'amount': 1,
}, {
'asset_id': 'rgb1bbb...',
'amount': 7,
},
]
},
'group_2': {
'label': 'asset group two',
'distribution': {
'mode': 2,
'random_params': {
'request_window_open': '2023-10-16T00:00:00+00:00',
'request_window_close': '2023-10-16T23:59:59+00:00',
}
}
'assets': [
{
'asset_id': 'rgb1ccc...',
'amount': 42,
}, {
'asset_id': 'rgb1ddd...',
'amount': 4,
},
]
},
}
See the Config
class in the faucet_rgb/settings.py
file for details on
configuration variables.
Wallets that received assets based on RGB v0.9 will lose them upon upgrading to RGB v0.10. Asset migration is a feature that allows such wallets to request an asset from the same asset group and receive the new version of the previous asset, re-issued with v0.10.
By default, when a request for assets from a specific group is received, if there is no previous request from the same wallet and group, a random asset from the selected group is sent, otherwise no asset is sent and an error is returned. This logic is still applied for asset groups that are not part of the migration configuration.
Configuring ASSET_MIGRATION_MAP
, asset groups that are included in the
configuration are no more part of the default logic. Instead, when a request
for assets from one such group is received, it will be checked against the
migration map. If the wallet ID matches a previous request for a v0.9 asset
being migrated, the new asset is sent, just once. Further requests by the same
wallet from the same group are denied.
Requesting from non-migration groups works as before, sending a random asset from the selected group. Requesting with no group specified works as before, sending a random asset from a random non-migration group.
For example, supposing the ASSET
declaration above was done for RGB v0.9
assets, after upgrading to v0.10, re-issuing the assets and including a new
group group_3
(which would operate with the default logic), it would become
something like:
ASSETS = {
'group_1': {
'label': 'asset group one',
'assets': [{
'asset_id': 'Nixon...1oA',
'amount': 1,
}, {
'asset_id': 'Visible...kEh',
'amount': 7,
}]
},
'group_2': {
'label': 'asset group two',
'assets': [{
'asset_id': 'Express...uwg',
'amount': 42,
}, {
'asset_id': 'Legacy...QBX',
'amount': 4,
}]
},
'group_3': {
'label': 'asset group three',
'assets': [{
'asset_id': 'Nato...Vnx',
'amount': 3,
}, {
'asset_id': 'Nadia...mXL',
'amount': 11,
}]
},
}
and the following migration map would allow migrating the old assets in
group_1
and group_2
:
ASSET_MIGRATION_MAP = {
'Nixon...1oA': 'rgb1aaa...',
'Visible...kEh': 'rgb1bbb...',
'Express...uwg': 'rgb1ccc...',
'Legacy...QBX': 'rgb1ddd...',
}
With this configuration, an example request for an asset from group_1
from a
wallet that was previously sent asset rgb1aaa
would trigger the sending of
asset Nixon...1oA
Note: when declaring ASSET_MIGRATION_MAP
, all assets in a group need to be
defined, partial migration for a group is not supported.
Endpoints require authentication via an API key, to be sent in the X-Api-Key
header.
There are two configurable API keys for authenticated requests:
API_KEY
: user requests (e.g./receive/<wallet_id>/<blinded_utxo>
)API_KEY_OPERATOR
: operator requests (e.g./receive/requests
)
APIs will return an {"error":"unauthorized"}
if the provided API key is
wrong.
The available endpoints are:
/control/assets
list assets/control/delete
delete failed transfers/control/fail
fail pending transfers/control/refresh/<asset_id>
requests a refresh for transfers of the given asset/control/transfers?status=<status>
list transfers, pending ones by default or in the status (rgb-lib's TransferStatus) provided as query parameter/control/unspents
returns the list of wallet unspents and related RGB allocations/reserve/top_up_btc
returns the first unused address of the faucet's bitcoin wallet/reserve/top_up_rgb
returns a blinded UTXO for the faucet's RGB wallet/receive/asset/<wallet_id>/<blinded_utxo>?asset_group=<asset_group>
sends the configured amount of a random asset in optional group<asset_group>
to<blinded_utxo>
; if noasset_group
is provided, a random asset from a non-migration group is chosen/receive/config/<wallet_id>
requests the faucet's configuration (name + groups), and the number of requests that are allowed for each group (only 1 or 0 are possible at the moment)1
if the user can request sending from this group (including migration)0
if the user cannot request from this group anymore
/receive/requests?asset_id=<asset_id>&blinded_utxo=<blinded_utco>&wallet_id=<wallet_id>
returns a list of received asset requests; can be filtered for<asset_id>
,<blinded_utxo>
or<wallet_id>
via query parameters
Notes:
<wallet_id>
needs to be a valid xpub
To install the dependencies excluding the production group:
poetry install --without production
To run the app in development mode:
poetry run flask --app faucet_rgb run --no-reload
Notes:
--no-reload
is required to avoid trying to restart RGB services, which fails trying to acquire a lock on open database files.- using
--debug
will prevent the scheduler from running
To test the development server (<wallet_id>
needs to be a valid xpub):
curl -i -H 'x-api-key: defaultapikey' localhost:5000/receive/config/<wallet_id>
To format and lint code use:
poetry run black faucet_rgb/ tests/
poetry run flake8 faucet_rgb/ tests/
poetry run pylint faucet_rgb/ tests/
poetry run vulture faucet_rgb/ tests/
Migrations are handles via flask-migrate
.
To modify the DB structure:
- change the DB (database.py)
- setup a minimal faucet configuration in
instance/config.py
- NAME
- XPUB (doesn't need to have funds)
- MNEMONIC (doesn't need to have funds)
- ASSETS (empty dict)
- run
poetry run flask --app faucet_rgb db migrate -m "<comment>"
- check the generated migration file (Alembic is not always able to detect every change to models)
- commit the DB changes along with the generated migration file
To install the dependencies excluding the dev group:
poetry install --sync --without dev
Example running the app in production mode:
export FAUCET_SETTINGS=</path/to/config.py>
poetry run waitress-serve --host=127.0.0.1 --call 'faucet_rgb:create_app'
To test the production server locally (<wallet_id>
needs to be a valid xpub):
curl -i -H 'x-api-key: defaultapikey' localhost:5000/receive/config/<wallet_id>
A Dockerfile
is available to build a docker image that runs the faucet. The
docker-compose.yml
file allows to run a faucet along with a complete regtest
environment.
The docker compose file uses the service_data
directory for service data and
faucet_data
as the data directory for the faucet. It also expects the file
config.py
(faucet configuration file) to exist. The faucet_data
dir needs
to be owned by user and group 1000. All paths are relative to the project root. Note that if the faucet is started via docker compose while
config.py`
doesn't exist, docker will create a directory with the same name instead, which
will need to be replaced with the correct file for the faucet to work.
Since the faucet needs to be configured before it can run (see the Initial
setup example section below), the docker_regtest_setup.sh
script is
available to automate such initial setup in docker for the simple case of a
regtest faucet distributing a single NIA asset.
Choose a directory to hold the faucet data (e.g. /srv/faucet
), create the
config.py
file inside it, then export the FAUCET_SETTINGS
environment
variable set to its path (e.g. export FAUCET_SETTINGS=/srv/faucet/config.py
).
Configure the DATA_DIR
and NETWORK
parameters, then create a new wallet:
poetry run wallet-helper --init
Configure the printed mnemonic
and xpub
using the related (uppercase)
variables, then generate an address and send some bitcoins (e.g. 10k sats), to
be used for creating UTXOs to hold RGB allocations:
poetry run wallet-helper --address
Once mnemonic and XPub have been configured, the wallet-helper
script can
also provide info on the wallet status, which might be useful during the
initial setup:
poetry run wallet-helper --unspents
poetry run wallet-helper --assets
Issue at least one asset. If no allocation slots are available, some will be created automatically. As an example:
Note: issuance in RGB may require the wallet to create new UTXOs, It means it must somehow deal with the blockchain. The default configuration is for testnet. Thus to run the following command for other networks requires you to specify
ELECTRUM_URL
inconfig.py
.
poetry run issue-asset NIA "fungible token" 0 1000 1000 --ticker "FFA"
poetry run issue-asset CFA "CTB" 0 10 10 --description "a collectible" --file_path ./README.md
Finally, complete the configuration by defining the faucet's NAME
and the
ASSETS
dictionary with the issued assets.
The docker
directory contains a docker compose to run local copies of the
services required by the faucet to operate, configured for the regtest network.
The services.sh
script is also included to start them:
./docker/services.sh start
The following services will be run in the background:
- bitcoind (regtest)
- electrs
- rgb-proxy-server
To configure the faucet to use these services, set the ELECTRUM_URL
and
TRANSPORT_ENDPOINTS
variables:
ELECTRUM_URL="tcp://localhost:50001"
TRANSPORT_ENDPOINTS=["rpc:http://localhost:3000/json-rpc"]
Regtest wallets can also be funded using services.sh
script:
./docker/services.sh fund <address> 1
Setup a faucet as in the Initial setup example section.
Using a different shell (as FAUCET_SETTINGS
will need to be exported to a
different path), data directory and configuration file, setup a separate
instance as described in the Initial setup example section, up to the wallet
funding part (stop before issuing assets).
This separate instance will be used as an RGB-enabled wallet to request assets
from the faucet.
To tear down the services, run:
./docker/services.sh stop
Generate a blinded UTXO with the request wallet:
poetry run wallet-helper --blind
Call the faucet's receive/asset
API using the request wallet xpub and the
generated blinded UTXO:
curl -i -H 'x-api-key: defaultapikey' localhost:5000/receive/asset/<xpub>/<blinded_utxo>
Automated integration testing is supported via pytest.
To execute tests, run:
poetry run pytest
To execute a single test module run:
poetry run pytest <path/to/testfile.py>
To execute a single test run:
poetry run pytest <path/to/testfile.py>::<test_name>
To enable code coverage (HTML report) run:
poetry run pytest --cov=faucet_rgb --cov-report=html
Notes:
- output capture can be disabled by adding the
-s
pytest option - output from passed tests can be shown at the end by adding the
-rP
pytest option - code coverate HTML report is saved in the
htmlcov
directory