publish
Publish a new asset.
Usage
import { , } from 'ethers'
import {
,
,
,
,
,
} from '@deltadao/nautilus'
const = new ('0x1234', new ('https://rpc.dev.pontus-x.eu'))
const = { : 'https://node.example.org' }
const = await .(, {
,
: new ({ : 'http://127.0.0.1:5001/api/v0/add' })
})
const = new <., .>({
: .
})
.('https://node.example.org')
.('Access Service')
.(86400)
.({ : 'url', : 'https://data.example/set.csv', : 'GET' })
.({ : 'free' })
.()
const = new ()
.('dataset')
.('My Dataset')
.('What it contains')
.('My Organisation')
.()
.()
const { , } = await .() What happens in order:
- The options and the requested lifecycle state are checked (
encrypt: falseand the statesDEPRECATED/REVOKED_BY_PUBLISHERthrow), anasset.ownerother than the signer is refused, and the remote store is resolved (NodePersistentRemoteStorethrows). AdatatokenAddressleft on a service is cleared:publish()always mints new datatokens. - Trusted algorithms are resolved to their container and file checksums, the services are checked, and the DDO is validated locally against the DDO v5 SHACL schema. All of this happens before any NFT exists, so a missing field costs no gas.
- The remote store's
check()runs, if it has one — forIpfsRemoteStorethatverify()will be able to read the envelope back, and the configuredprobe, forS3RemoteStorea write-and-read-back of both keys. A store that would refuse the upload fails here, still before any transaction. nautilus also warns, once, when the publisher's address is the node's own (providerAddress): the node's decrypt calls then fail with 401 and the asset is never indexed. - The NFT, the first service's datatoken and that service's pricing are created in one transaction. Each further service gets its own datatoken on the same NFT.
- Each service's file object is encrypted by the node, and the final DDO is built. Its
idmust be the NFT'sdid:ope:DID, and it is validated again. - The DDO is signed as a verifiable credential, and the signed payload is compared with
the validated DDO (see writing your own signer).
The node encrypts it into an envelope, the metadata hash is computed as the sha256 of that
envelope, and the envelope is stored with the remote store's
put(). - The pointer
put()returned is validated (typeipfs,url,s3,arweaveorftp, with the fields the node needs for that type, e.g. a CIDv0 or CIDv1hashforipfs), wrapped in{ remote }and encrypted by the node. - The store's
verify()runs, if it has one: it reads the stored envelope back the way the node will and checks its hash.S3RemoteStorereads with the read key;IpfsRemoteStorethrough itsgatewayUrl, or a Kubo node's/api/v0/cat, and skips it only with an explicitverify: false(itscheck()in step 3 refuses a store that could not read back). - Right before the transaction, nautilus asserts that the pointer is node ciphertext under
flags
0x02, that the node written on chain is the one that encrypted, and that neither the requested state nor the NFT's state on chain isDEPRECATEDorREVOKED_BY_PUBLISHER. Then the pointer is written onto the NFT: nautilus builds the transaction with ocean.js and sends it with the signer, so it can tell a wallet rejection and a mined revert, after which no transaction points at the stored envelope and it is removed again, from a transaction that may have been mined. A failed send whose error carries the transaction's hash (ethers setsinfo.sendTransactionHashwhen it gives up polling for a transaction the wallet already broadcast) counts as sent, whatever its code. A transaction the wallet sped up (same call, higher fee) counts as this one: its replacement's receipt is returned, or its revert reported. On a confidential chain (sdk: 'oasis') the send stays with ocean.js, which encrypts it, and cannot tell these apart. - After the receipt, nautilus checks that block for another metadata event of the same NFT
and throws a
MetadataConflictErrorif there is one, because ocean-node indexes only the first. - With
waitForIndexer, it waits until the node has indexed the asset.
Everything that goes to the store or on chain is node-encrypted; there is no plaintext mode. See how the node resolves your DDO.
Returns
- Type:
PublishResponse
Parameters
asset
- Type:
NautilusAsset
Built with the AssetBuilder.
options (optional)
- Type:
PublishOptions
Properties
waitForIndexer
- Type:
boolean | { intervalMs?: number; timeoutMs?: number; signal?: AbortSignal } - Default:
false
Block until ocean-node's indexer has the asset, so the result is immediately resolvable.
Sets indexed: true on the response. Pass an object to tune the polling: intervalMs
defaults to 7 seconds (at most 18 requests a minute, under ocean-node's default rate limit
of 30), timeoutMs to 5 minutes; aborting signal stops the wait and rejects with its
reason. A rate-limited poll backs off instead of failing; see
waitForIndexer.
const { } = await .(, { : true })
await .(, {
: { : 5_000, : 600_000 }
}) When the node records that it could not index the asset, this throws an IndexingError
carrying the node's message. On timeout it throws an OceanNodeError. Either way, the asset
is already on chain, and the error carries the full response as error.published; an abort
reason that cannot take it (a DOMException, a string) is wrapped in an Error that does,
with the reason as cause. See
waitForIndexer.
remoteStore / ddoSigner
Override the instance's remote store or DDO signer for this call.
checkIndexerNonce
- Type:
boolean - Default:
true
Before the first transaction, read the node's indexer nonce with
getIndexerNonceState() (two GETs,
nothing signed) and throw an IndexerNonceStuckError when the node would not accept its
indexer's next decrypt call (stuck: true): the asset would go on chain without being
indexed. Ask the node operator to advance the stored nonce of the node's address. A node
that does not answer the check (P2P, an older node, a network error) is not an error: the
check is skipped. It also logs a warning when this publish's own second decrypt call would
use a nonce the node does not accept. false skips the check.
Which nonces a node does not accept depends on its version (the version of its GET /):
before 4.2.2 it refuses a decrypt call whose signed message hash starts with a zero byte,
so a next nonce with such a hash sticks the indexer; 4.2.2 and later accept it, so it is
neither refused nor warned about. Instead nautilus logs node.indexer_nonce.zero_byte_accepted
at info, once per node and nonce. A node that reports no version, or a pre-release of 4.2.2, is treated as older.
Warnings
publish() goes on after these and prints a warning: on stderr by default, or to your
sink (see configureLogging). They print at the
default level (warn). Filter on the event name, not on the message, which may change.
Errors, URLs and node text in a warning are redacted and shortened: an RPC URL shows only its
origin, so its key is never printed. With the debug channel on,
the removed text goes there.
| Event | What it means | What to do | How often |
|---|---|---|---|
publish.preflight.denies_all | A node that checks the asset has a policy server, and the asset's credentials have no address allow list. That is the configured node or any service's serviceEndpoint. The policy server checks the consumer's address against that list first, so it refuses every consumer. The asset is published, but nobody can download it or run compute on it. | Add the consumers' addresses with addCredentialAddresses(CredentialListTypes.ALLOW, [...]) before publishing. | Once per node and asset. The asset is its DID when it has one (edit, completePublish). Before the mint there is no DID, so it is the publisher's address and the asset's name: new assets of one publisher with the same name warn once per node. Publishing an asset and then editing it warns once for each. |
publish.preflight.publisher_is_node | The publisher's address is the node's own (providerAddress). The node's decrypt calls then fail with 401, and the asset goes on chain but is never indexed. | Publish with another key. | Once per node address. |
publish.preflight.indexer_nonce_will_stick | The node's indexer will sign this asset's second decrypt call with a nonce the node does not accept. Unless another decrypt moves the nonce first, the asset is not indexed and the indexer nonce gets stuck. | Ask the node operator, or publish later. See checkIndexerNonce. | Once per node and nonce. |
publish.metadata.events_unchecked | The block of the metadata transaction could not be read, so a second metadata event of the NFT in the same block would go unnoticed (see MetadataConflictError below). | Use waitForIndexer and check that the indexed asset is yours. | Every time. |
publish.preflight.pending_unreadable | completePublish only: the signer's pending transaction count could not be read, so an earlier metadata transaction that is still pending cannot be ruled out. | Check that no earlier metadata transaction of the NFT is still pending. If one is, it may write the metadata a second time. | Every time. |
To silence them, pass level: 'error' to configureLogging,
or handle them in your own sink.
Throws
- Before any transaction:
encrypt: false, aDEPRECATEDorREVOKED_BY_PUBLISHERlifecycle state, anasset.ownerother than the signer, an asset without services, a missing or unsupported remote store, a failing storecheck(), an invalid DDO, a DDO too large for the node to decrypt (its signed envelope could exceed the node's 100 KB decrypt request limit, counting the encrypted files and up to 1024 characters of JWS header and signature), an unreachable service endpoint or file, or anIndexerNonceStuckError(seecheckIndexerNonce). PublishIncompleteError, when something fails after the NFT was minted — a later datatoken, the remote store'sput()orverify(), the pointer validation, the node, the RPC, or a signed DDO that turns out too large for the node to decrypt (checked before it is stored). It carriesnftAddress, thedatatokenscreated so far and the original error oncause. PassnftAddressand the same asset tocompletePublishto finish the publish on that NFT. When the envelope had already been stored,storedsays what became of it: removed again (the metadata transaction was never sent, or was mined and reverted), left because the store could not remove it, or kept because the transaction was sent and may still be mined — then wait for it before completing, and passstored.txHashtocompletePublishasmetadataTxHash. See when a write fails. A transaction that failed to send is named in the error: the RPC's refusal, or aNonceGuardErrorwhen it was refused for its nonce and nautilus did not, or could not safely, send it again, or when a resend failed for another reason (see Concurrency and nonces), oncause. A metadata transaction refused for its nonce leaves the envelope only when it is proven never mined, with its nonce taken by another transaction in a finalized block (cleanup: 'removed'); otherwise'kept'.MetadataConflictError, when another metadata transaction for the NFT landed in the same block, andIndexingError/OceanNodeError, fromwaitForIndexer, when the node could not index the asset or did not within the timeout. These are not wrapped in aPublishIncompleteError: the metadata is on chain by then, so there is nothing left to complete. Each carries the fullPublishResponseaserror.published(typePublishedNotIndexed) — seePublishResponse.