Skip to content
LogoLogo

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:

  1. The options and the requested lifecycle state are checked (encrypt: false and the states DEPRECATED/REVOKED_BY_PUBLISHER throw), an asset.owner other than the signer is refused, and the remote store is resolved (NodePersistentRemoteStore throws). A datatokenAddress left on a service is cleared: publish() always mints new datatokens.
  2. 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.
  3. The remote store's check() runs, if it has one — for IpfsRemoteStore that verify() will be able to read the envelope back, and the configured probe, for S3RemoteStore a 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.
  4. 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.
  5. Each service's file object is encrypted by the node, and the final DDO is built. Its id must be the NFT's did:ope: DID, and it is validated again.
  6. 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().
  7. The pointer put() returned is validated (type ipfs, url, s3, arweave or ftp, with the fields the node needs for that type, e.g. a CIDv0 or CIDv1 hash for ipfs), wrapped in { remote } and encrypted by the node.
  8. The store's verify() runs, if it has one: it reads the stored envelope back the way the node will and checks its hash. S3RemoteStore reads with the read key; IpfsRemoteStore through its gatewayUrl, or a Kubo node's /api/v0/cat, and skips it only with an explicit verify: false (its check() in step 3 refuses a store that could not read back).
  9. 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 is DEPRECATED or REVOKED_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 sets info.sendTransactionHash when 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.
  10. After the receipt, nautilus checks that block for another metadata event of the same NFT and throws a MetadataConflictError if there is one, because ocean-node indexes only the first.
  11. 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

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.

EventWhat it meansWhat to doHow often
publish.preflight.denies_allA 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_nodeThe 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_stickThe 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_uncheckedThe 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_unreadablecompletePublish 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, a DEPRECATED or REVOKED_BY_PUBLISHER lifecycle state, an asset.owner other than the signer, an asset without services, a missing or unsupported remote store, a failing store check(), 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 an IndexerNonceStuckError (see checkIndexerNonce).
  • PublishIncompleteError, when something fails after the NFT was minted — a later datatoken, the remote store's put() or verify(), 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 carries nftAddress, the datatokens created so far and the original error on cause. Pass nftAddress and the same asset to completePublish to finish the publish on that NFT. When the envelope had already been stored, stored says 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 pass stored.txHash to completePublish as metadataTxHash. See when a write fails. A transaction that failed to send is named in the error: the RPC's refusal, or a NonceGuardError when 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), on cause. 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, and IndexingError / OceanNodeError, from waitForIndexer, when the node could not index the asset or did not within the timeout. These are not wrapped in a PublishIncompleteError: the metadata is on chain by then, so there is nothing left to complete. Each carries the full PublishResponse as error.published (type PublishedNotIndexed) — see PublishResponse.