This step is the fun one! In it, you'll publish a data asset, post for free / for sale, dispense it / buy it, and consume it.
We assume you've already (a) installed Ocean, and (b) done local setup or remote setup. This flow works for either one, without any changes between them (!)
Steps in the flow:
- Alice publishes dataset
- Bob gets access to the dataset (faucet, priced, etc)
- Bob consumes the dataset
Let's go!
In the same Python console:
#data info
name = "Branin dataset"
url = "https://raw.githubusercontent.com/trentmc/branin/main/branin.arff"
#create data asset
(data_nft, datatoken, ddo) = ocean.assets.create_url_asset(name, url, {"from": alice})
#print
print("Just published asset:")
print(f" data_nft: symbol={data_nft.symbol}, address={data_nft.address}")
print(f" datatoken: symbol={datatoken.symbol}, address={datatoken.address}")
print(f" did={ddo.did}")
You've now published an Ocean asset!
data_nft
is the base (base IP)datatoken
for access by others (licensing)ddo
holding metadata
(For more info, see Appendix: Publish Details.)
Bob wants to consume the dataset that Alice just published. The first step is for Bob to get 1.0 datatokens. Below, we show four possible approaches:
- A & B are when Alice is in contact with Bob. She can mint directly to him, or mint to herself and transfer to him.
- C is when Alice wants to share access for free, to anyone
- D is when Alice wants to sell access
In the same Python console:
from ocean_lib.ocean.util import to_wei
#Approach A: Alice mints datatokens to Bob
datatoken.mint(bob, to_wei(1), {"from": alice})
#Approach B: Alice mints for herself, and transfers to Bob
datatoken.mint(alice, to_wei(1), {"from": alice})
datatoken.transfer(bob, to_wei(1), {"from": alice})
#Approach C: Alice posts for free, via a dispenser / faucet; Bob requests & gets
datatoken.create_dispenser({"from": alice})
datatoken.dispense(to_wei(1), {"from": bob})
#Approach D: Alice posts for sale; Bob buys
# D.1 Alice creates exchange
price = to_wei(100)
exchange = datatoken.create_exchange({"from": alice}, price, ocean.OCEAN_address)
# D.2 Alice makes 100 datatokens available on the exchange
datatoken.mint(alice, to_wei(100), {"from": alice})
datatoken.approve(exchange.address, to_wei(100), {"from": alice})
# D.3 Bob lets exchange pull the OCEAN needed
OCEAN_needed = exchange.BT_needed(to_wei(1), consume_market_fee=0)
ocean.OCEAN_token.approve(exchange.address, OCEAN_needed, {"from":bob})
# D.4 Bob buys datatoken
exchange.buy_DT(to_wei(1), consume_market_fee=0, tx_dict={"from": bob})
(For more info, see Appendix: Dispenser / Faucet Details and Exchange Details.)
Bob now has the datatoken for the dataset! Time to download the dataset and use it.
In the same Python console:
# Bob sends a datatoken to the service to get access
order_tx_id = ocean.assets.pay_for_access_service(ddo, {"from": bob}).hex()
# Bob downloads the file. If the connection breaks, Bob can try again
asset_dir = ocean.assets.download_asset(ddo, bob, './', order_tx_id)
import os
file_name = os.path.join(asset_dir, "file0")
Let's check that the file is downloaded. In a new console:
cd my_project/datafile.did:op:*
cat file0
The beginning of the file should contain the following contents:
% 1. Title: Branin Function
% 3. Number of instances: 225
% 6. Number of attributes: 2
@relation branin
@attribute 'x0' numeric
@attribute 'x1' numeric
@attribute 'y' numeric
@data
-5.0000,0.0000,308.1291
-3.9286,0.0000,206.1783
...
(For more info, see Appendix: Consume Details.)
You've now gone through the main flow for Ocean, congrats!
Where you want to go next is up to you! Some possibilities:
- Go back to the top-level README to explore advanced flows like Compute-to-Data, Predict-ETH, and Data Farming
- Review this README's appendices, which expand on the steps above with further flexibility.
Anytime in the future, you can reconstruct your data NFT as an object in Python, via:
from ocean_lib.models.data_nft import DataNFT
config = <like shown elsewhere in READMEs>
data_nft_address = <what you wrote down previously>
data_nft = DataNFT(config, data_nft_address)
It's similar for Datatokens. In Python:
from ocean_lib.models.datatoken_base import DatatokenBase
config = <like shown elsewhere in READMEs>
datatoken_address = <what you wrote down previously>
datatoken = DatatokenBase.get_typed(config, datatoken_address)
Data NFTs implement ERC721 functionality, ERC725 which extends it, and data management functionality on top.
ERC721:
- Basic spec of a non-fungible token (NFT)
- Official spec is at erc721.org
- Solidity interface: IERC721Template.sol
ERC725:
- ERC725X is execution, and Y is key-value store
- Official spec is at eips.ethereum.org
- Solidity interface: IERC725X.sol (execution) and IERC725Y.sol (key-value store)
The data_nft
is a Python object of class DataNFT. The DataNFT class directly exposes the Solidity ERC721 & ERC725 interfaces. This means your data_nft
object has a Python method for every Solidity method!
Besides that, DataNFT implements other Python methods like create_datatoken()
to improve developer experience. And, ocean_assets.OceanAssets and other higher-level Python classes / methods work with DataNFT.
Ocean's architecture allows for >1 implementations of ERC721, each with its own Solidity template and Python class. Here are the templates:
- ERC721Template.sol, exposed as Python class
DataNFT
- (there's just one template so far; we can expect more in the future)
Datatokens implement ERC20 functionality, and data management functionality on top.
ERC20:
- Basic spec of a fungible token standard
- Official spec is at eips.ethereum.org
- Solidity interface: IERC20Template.sol
Ocean's architecture allows for >1 implementations of ERC20, each with its own "template". Here are the templates so far (we can expect more over time).
Template 1:
- Solidity: ERC20Template.sol
- Python wrapper: Datatoken1. It has a Python method for every Solidity method.
- Implements methods like
start_order()
,create_exchange()
, andcreate_dispenser()
to enhance developer experience.
Template 2:
- Inherits from template 1 in both Solidity and Python.
- Solidity: ERC20TemplateEnterprise.sol
- Python wrapper: Datatoken2
- New method:
buy_DT_and_order()
. This uses just 1 tx to do both actions at once (versus 2 txs for template 1). - New method:
dispense_and_order()
. Similarly, uses just 1 tx.
Below you can find an explanatory table describing the template attributes:
Template # | Class Label | Allows dispense by default? | Allows non-custody of datatokens? | Combines txs? | Allows non-custody of url? |
---|---|---|---|---|---|
1 | Datatoken1 | Y | N | N | N |
2 | Datatoken2 | N | Y | Y | N |
DDOs get returned in create()
calls. Think of them as metadata, following a well-defined format.
Let's get more specific. A DID is a decentralized identifier. A DID Document (DDO) is a JSON blob that holds information about the DID. Given a DID, a resolver will return the DDO of that DID.
An Ocean asset has a DID and DDO, alongside a data NFT and >=0 datatokens. The DDO should include metadata about the asset, and define access in at least one service. Only owners or delegated users can modify the DDO.
DDOs follow a schema - a pre-specified structure of possible metadata fields.
Ocean Aquarius helps in reading, decrypting, and searching through encrypted DDO data from the chain.
Ocean docs have further info yet.
Here's an example similar to the create()
step above, but exposes more fine-grained control.
In the same python console:
# Specify metadata and services, using the Branin test dataset
date_created = "2021-12-28T10:55:11Z"
metadata = {
"created": date_created,
"updated": date_created,
"description": "Branin dataset",
"name": "Branin dataset",
"type": "dataset",
"author": "Trent",
"license": "CC0: PublicDomain",
}
# Use "UrlFile" asset type. (There are other options)
from ocean_lib.structures.file_objects import UrlFile
url_file = UrlFile(
url="https://raw.githubusercontent.com/trentmc/branin/main/branin.arff"
)
# Publish data asset
from ocean_lib.models.datatoken_base import DatatokenArguments
_, _, ddo = ocean.assets.create(
metadata,
{"from": alice},
datatoken_args=[DatatokenArguments(files=[url_file])],
)
The DDO is stored on-chain. It's encrypted and compressed by default. Therefore it supports GDPR "right-to-be-forgotten" compliance rules by default.
You can control this during create():
- To disable encryption, use
ocean.assets.create(..., encrypt_flag=False)
. - To disable compression, use
ocean.assets.create(..., compress_flag=False)
. - To disable both, use
ocean.assets.create(..., encrypt_flag=False, compress_flag=False)
.
Calling create()
like above generates a data NFT, a datatoken for that NFT, and a ddo. This is the most common case. However, sometimes you may want just the data NFT, e.g. if using a data NFT as a simple key-value store. Here's how:
data_nft = ocean.data_nft_factory.create({"from": alice}, 'NFT1', 'NFT1')
If you call create()
after this, you can pass in an argument data_nft_address:string
and it will use that NFT rather than creating a new one.
Calling create()
like above generates a data NFT, a datatoken for that NFT, and a ddo object. However, we may want a second datatoken. Or, we may have started with just the data NFT, and want to add a datatoken to it. Here's how:
datatoken = data_nft.create_datatoken({"from": alice}, "Datatoken 1", "DT1")
If you call create()
after this, you can pass in an argument deployed_datatokens:List[Datatoken1]
and it will use those datatokens during creation.
We access dispenser (faucet) functionality from two complementary places: datatokens, and Dispenser
object.
A given datatoken can create exactly one dispenser for that datatoken.
Interface via datatokens:
datatoken.create_dispenser()
- implemented in DatatokenBase, inherited by Datatoken2datatoken.dispense()
- ""datatoken.dispense_and_order()
- implemented in Datatoken1 and in Datatoken2. The latter only needs one tx to dispense and order.datatoken.dispenser_status()
- returns aDispenserStatus
object
Interface via Dispenser
Python class:
- You can access its instance via
ocean.dispenser
. Dispenser
wraps Dispenser.sol Solidity implementation, to exposeDispenser.sol
's methods as Python methods.- Note that
Dispenser
is global across all datatokens. Therefore calls to the Solidity contract - or Python calls that pass through - provide the datatoken address as an argument. - Example call:
ocean.dispenser.setAllowedSwapper(datatoken_addr, ZERO_ADDRESS, {"from": alice})
datatoken.create_dispenser()
can take these optional arguments:
max_tokens
- maximum number of tokens to dispense. The default is a large number.max_balance
- maximum balance of requester. The default is a large number.with_mint
- allow mintingallowed_swapper
- swapper user address
A call would look like create_dispenser({"from": alice}, max_tokens=max_tokens, max_balance=max_balance)
.
To learn about dispenser status:
status = datatoken.dispenser_status()
print(f"For datatoken {datatoken.address}:")
print(status)
It will output something like:
For datatoken 0x92cA723B61CbD933390aA58b83e1F00cedf4ebb6:
DispenserStatus:
active = True
owner_address = 0x1234
is_minter = True
max_tokens = 1000 (10000000000000000000000 wei)
max_balance = 10 (100000000000000000000 wei)
balance = 1
allowed_swapper = anyone can request
Template 1 (Datatoken1
):
- Anyone can call
datatoken.dispense()
to request tokens.
Template 2 (Datatoken2
):
- Option A. Anyone can
datatoken.dispense_and_order()
to request tokens, and order. - Option B. Not anyone can call
datatoken.dispense()
by default. To allow anyone, the publisher does:ocean.dispenser.setAllowedSwapper(datatoken_address, ZERO_ADDRESS, {"from" : publisher_wallet})
, whereZERO_ADDRESS
is0x00..00
.
Details: Dispenser.sol
has an attribute allowed_swapper
to govern who can call dispense()
. A value of 0x00...0
allows anyone. Template 1 has ZERO_ADDRESS
as a default value; template 2 does not. However, template 2 allows anyone to call dispense_and_order()
, independent of the value of allowed_swapper
.
We access exchange functionality from three complementary places: datatokens, OneExchange
object, and (if needed) FixedRateExchange
object.
A given datatoken can create one or more OneExchange
objects.
Interface via datatokens:
datatoken.create_exchange()
- Returns aOneExchange
object.datatoken.get_exchanges()
- Returns a list ofOneExchange
objects.
Once you've got a OneExchange
object, most interactions are with it.
Interface via OneExchange
Python class:
BT_needed()
- # basetokens (BTs) needed, to buy target # datatokens (DTs)BT_received()
- # BTs you receive, in selling given # DTsbuy_DT()
- spend BTs to buy DTssell_DT()
- sell DTs, receive back BTs- and more, including get/set rate (price), toggle on/off, get/set fees, update balances
While most interactions are with OneExchange
described above, sometimes we may want to access the FixedRateExchange
object.
Interface via FixedRateExchange
Python class:
- You can access its instance via
ocean.fixed_rate_exchange
. FixedRateExchange
wraps FixedRateExchange.sol Solidity implementation, to exposeDispenser.sol
's methods as Python methods.- Note that
FixedRateExchange
is global across all datatokens. Therefore calls to the Solidity contract - or Python calls that pass through - provide the exchange id address as an argument. It's exchange id, and not datatoken, because there may be >1 exchange for a given datatoken. - Example call:
ocean.fixed_rate_exchange.getRate(exchange_id)
returns rate (price).
When Alice posted the dataset for sale via create_exchange()
, she used OCEAN. Alternatively, she could have used H2O, the OCEAN-backed stable asset. Or, she could have used USDC, DAI, RAI, WETH, or other, for a slightly higher fee (0.2% vs 0.1%).
Here's how to see all the exchanges that list the datatoken. In the Python console:
exchanges = datatoken.get_exchanges() # list of OneExchange
To learn more about the exchange status:
print(exchange.details)
print(exchange.exchange_fees_info)
It will output something like:
>>> print(exchange.details)
ExchangeDetails:
datatoken = 0xdA3cf7aE9b28E1A9B5F295201d9AcbEf14c43019
base_token = 0x24f42342C7C171a66f2B7feB5c712471bED92A97
fixed_rate (price) = 1.0 (1000000000000000000 wei)
active = True
dt_supply = 99.0 (99000000000000000000 wei)
bt_supply = 1.0 (1000000000000000000 wei)
dt_balance = 0.0 (0 wei)
bt_balance = 1.0 (1000000000000000000 wei)
with_mint = False
dt_decimals = 18
bt_decimals = 18
owner = 0x02354A1F160A3fd7ac8b02ee91F04104440B28E7
>>> print(exchange.exchange_fees_info)
ExchangeFeeInfo:
publish_market_fee = 0.0 (0 wei)
publish_market_fee_available = 0.0 (0 wei)
publish_market_fee_collector = 0x02354A1F160A3fd7ac8b02ee91F04104440B28E7
opc_fee = 0.001 (1000000000000000 wei)
ocean_fee_available (to opc) = 0.001 (1000000000000000 wei)
Any of the ocean.assets.create_<type>_asset()
functions can also take an optional parameter that describes a bundled pricing schema (Dispenser or Fixed Rate Exchange).
This can be either a DispenserArguments or an ExchangeArguments object. The parameters for these Arguments classes are identical to those for creating the object itself.
E.g. adding pricing_schema_args=DispenserArguments(to_wei(1), to_wei(1))
to the create
function is equivalent to performing the creation and creating a dispenser later using dt.create_dispenser(to_wei(1), to_wei(1))
.
Here is an example involving an exchange:
from ocean_lib.models.fixed_rate_exchange import ExchangeArguments
(data_nft, datatoken, ddo) = ocean.assets.create_url_asset(
name,
url,
{"from": alice},
pricing_schema_args=ExchangeArguments(rate=to_wei(3), base_token_addr=ocean.OCEAN_address, dt_decimals=18)
)
assert len(datatoken.get_exchanges()) == 1
To "consume" an asset typically means placing an "order", where you pass in 1.0 datatokens and get back a url. Then, you typically download the asset from the url.
For more information, search for "order" in this README or related code.
The file is in ARFF format, used by some AI/ML tools. Our example has two input variables (x0, x1) and one output.
% 1. Title: Branin Function
% 3. Number of instances: 225
% 6. Number of attributes: 2
@relation branin
@attribute 'x0' numeric
@attribute 'x1' numeric
@attribute 'y' numeric
@data
-5.0000,0.0000,308.1291
-3.9286,0.0000,206.1783
...
At the beginning of most flows, we create an ocean
object, which is an instance of class Ocean
. It exposes useful information, including the following.
Config dict attribute:
ocean.config_dict
orocean.config -> dict
OCEAN token:
ocean.OCEAN_address -> str
ocean.OCEAN_token
orocean.OCEAN -> Datatoken
Ocean smart contracts:
ocean.data_nft_factory -> DataNFTFactoryContract
ocean.dispenser -> Dispenser
- faucets for free dataocean.fixed_rate_exchange -> FixedRateExchange
- exchanges for priced data
Simple getters:
ocean.get_nft_token(self, token_address: str) -> DataNFT
ocean.get_datatoken(self, token_address: str) -> Datatoken
ocean.def get_user_orders(self, address: str, datatoken: str)
- (and some others that are more complex)
It also provides Python wrappers to veOCEAN and Data Farming contracts. See df.md for details.