Skip to content
LogoLogo

Custom Configuration

nautilus resolves the chain from your signer's provider and then merges ocean.js's default config for that chain with whatever you pass in config.

import { ,  } from 'ethers'
import {  } from '@deltadao/nautilus'
 
const  = new ('0x1234', new ('https://rpc.dev.pontus-x.eu'))
 
const  = await .(, {
  : { 
    : 'https://ocean-node.dev.pontus-x.eu'
  } 
})

What changed from v1

The Config type moved with the stack. Three fields nautilus v1 relied on are gone:

v1 fieldv2
metadataCacheUrioceanNodeUri — one node serves metadata, provider services and the indexer
providerUrioceanNodeUri — same node
subgraphUriremoved — pricing is derived from chain reads and the DDO's indexed stats

New fields worth knowing about: escrow (compute payments), accessListFactory (access-list gating), and sdk, which is 'oasis' on confidential EVMs and 'evm' elsewhere.

The ocean-node URI

The single most important setting. One ocean-node replaces the old Aquarius plus Provider pair, so this is where metadata resolution, file encryption, download URLs and compute all go:

const : <> = {
  : 'https://ocean-node.dev.pontus-x.eu'
}

A peer id or multiaddr also works instead of an HTTP URL, in which case ocean.js talks to the node over libp2p rather than REST.

An HTTP URL must be https://, except on localhost, 127.0.0.0/8, ::1 and *.localhost: the plaintext DDO pointer and the signed DDO go to this node for encryption, and node auth travels with every request. Nautilus.create throws for a plain http:// URI elsewhere unless you pass allowInsecureTransport: true next to config.

The URL is judged the way fetch parses it, so spellings such as http:host or http:/host count as plain http://, and a URL containing a tab, a line break or another control character is refused. *.localhost names are trusted without being resolved; they are reserved for loopback (RFC 6761), but if your resolver does not map them there, use localhost or 127.0.0.1 instead.

It must be an OceanProtocolEnterprise ocean-node 4.2.x — see which node. For access to the deltaDAO test environment, or to have your own ocean-node run as a service, contact deltaDAO at delta-dao.com/contact or contact@delta-dao.com.

The same node encrypts your DDO and its pointer, and its URI is written on chain as the node that can decrypt them, so keep publishing and editing an asset through the same node.

The RPC endpoint

nodeUri is the blockchain RPC, not the ocean-node — an easy pair to confuse. In practice you rarely set it, because nautilus uses the provider already attached to your signer for every transaction.

const : <> = {
  : 'https://rpc.dev.pontus-x.eu'
}

Concurrency and nonces

Every transaction nautilus sends goes through a nonce guard that Nautilus.create wraps around your signer. It exists because ocean.js swallows a failed send and returns null, and because each send used to read its nonce from the RPC on its own: two publishes, a publish and an order, or two orders of one key running at once could read the same nonce, and one of them failed with only "ocean.js returned no transaction"; so did a send after a stale nonce read from the RPC.

With the default nonceManagement: 'auto' and a signer that signs locally (an ethers Wallet):

  • One send at a time per chain and address, in this process: building the transaction and broadcasting it are serialised, waiting for it to be mined is not. Concurrent calls of one instance, and of several instances with the same key, get consecutive nonces.
  • The nonce is max(nautilus's counter, the RPC's pending count): a pending count the RPC answers from stale state is masked once nautilus has sent with the key. The counter advances only after a successful broadcast, so a refused send leaves no gap. A send takes the pending count instead (chain.nonce.counter_reset) only when the earliest of nautilus's transactions that the pending count does not cover was dropped from the pool: sent more than a minute ago, unknown by hash on two reads, and the account has not mined past its nonce. That fills the gap a dropped transaction leaves before the ones still waiting behind it; the send after it skips those. A send that fails to fill the gap leaves it to the next send. An RPC that merely lags right after a broadcast does not trigger it.
  • Recovery from nonce refusals only (NONCE_EXPIRED / "nonce too low", REPLACEMENT_UNDERPRICED, "already known"), and only for a refused transaction that is provably this send's (same sender, recipient and data):
    • the chain already has it by hash: nautilus waits for that one and never sends it again (chain.nonce.already_sent);
    • it does not: the same transaction is sent again with a fresh nonce, the next one the RPC names, else the highest of the counter, pending and latest; at most three attempts, 0.5 to 2 seconds apart (chain.nonce.resending);
    • an order, purchase, approval or escrow payment (any call not known to move no value) is sent again only with proof from two RPCs that the first one can never be mined: the account's mined count past its nonce on both, its hash unknown on both, and, in the blocks since the send on each RPC, another transaction of the account holding that nonce in a finalized block. One RPC's blocks are not enough (it may be on another fork); a holder only in the unfinalized tip is not either (a reorganisation can drop it and mine the order in its slot); a block either RPC does not return, an RPC without a finalized block, a read that fails, or the transaction itself in a block, is no proof. Nor is a slot holder that makes the same call (same contract, same function): it may be this order sped up in a wallet or retried by another process, so sending it again could pay twice. nautilus does not wait for finality: on a chain that finalizes in minutes (an OP Stack chain), a collision of a moment ago stops with "may have been sent" too. Without a verificationProvider it is never sent again. Otherwise nautilus stops with a NonceGuardError whose reason is 'may-have-been-sent': check the account's transactions before retrying. A nonce another transaction took before the send (a stale pending read, the common case) is not in those blocks, so such an order stops too: a false "may have been sent" costs a manual check, a false "never sent" a second payment.
  • Never a retry after a timeout, a network error or any other refusal: the transaction may have landed.
  • The cause is named. When ocean.js returns no transaction, the error says why (startOrder failed: …REPLACEMENT_UNDERPRICED…), with the guard's error or the RPC's refusal as cause, and adds "It may have been sent" unless the send was refused before its broadcast. The cause is the failure of the call's own contract: when two sends to one contract are on record (two orders of one asset in Promise.all; a broadcast stays on record until it is mined, a failure for a minute), nautilus cannot tell which call failed and keeps the generic "ocean.js returned no transaction" for both. The cause is the guard's NonceGuardError, or a sanitised copy of the wallet's or RPC's error (name, code, shortMessage and its message, redacted). The error a send throws is redacted too: no signed transaction (an unmined one can be broadcast by anyone who reads it), no RPC URL path or query (it may hold the RPC's API key), no HTTP request or response body. It is redacted in place; an error that does not let its fields be changed (a frozen error, a read-only field) is thrown as a sanitised copy instead, with its name, code, reason, the transaction hashes and its messages, redacted. The raw values go to the debug channel (topic nonce).
const  = await .(, {
  : 'auto', // the default
  : new ('https://rpc2.example.org') 
})
 
// Safe from one key in one process: nonces come out consecutive.
const  = await .(.(() => .()))
 
for (const  of )
  if (
    . === 'rejected' &&
    .?.cause instanceof  &&
    ..cause.reason === 'may-have-been-sent'
  )
    .('check the account before retrying:', ..cause.attempts)

Set verificationProvider for paid flows. It must be an independent RPC for the same chain: create checks the chain id, and refuses the signer's own provider object or a provider with the same URL (one RPC read twice proves nothing a second RPC would). The proofs above trust that "unknown by hash" means "not sent"; a single, possibly load-balanced RPC whose transaction index lags its state can answer that for a transaction it already mined. That is why a transaction that moves value is never sent again without a second RPC, and why the metadata cleanup reads both when one is set.

Wallet-managed signers (JsonRpcSigner, a browser wallet through BrowserProvider) assign nonces themselves: nautilus sets none and sends nothing again, but still returns a refused transaction the chain already has and names the cause of a failed send. So does nonceManagement: 'off', for any signer.

The lock is held while a transaction is built and broadcast, not while it is mined. A broadcast that hangs holds it until the transport gives up (ethers' JsonRpcProvider after 5 minutes): later sends of the same key wait for it. An EIP-1193 or custom provider without a timeout can hold it indefinitely; give such a provider a request timeout.

NonceGuardError

What the guard throws when it stops after a nonce refusal. On the ocean.js path (an order, a datatoken, pricing) it is the cause of the operation's error; for the metadata transaction it is the error itself (inside a PublishIncompleteError on publish).

FieldMeaning
reason'may-have-been-sent': not provably unsent, it may still be mined (also when a resend failed for another reason, e.g. a timeout); 'attempts-exhausted': refused three times; 'unverifiable': the refusal carries no signed transaction of this send; 'not-managed': the nonce was set by the caller, by the wallet, or nonceManagement is 'off'
codethe ethers code of the last failure, e.g. NONCE_EXPIRED
attempts{ hash, nonce } of each refused signed transaction, to look up before retrying
attemptsCompletewhether attempts lists every transaction the send may have broadcast; false after an unverifiable refusal or a resend that may have gone out (look the account's transactions up by nonce then)
moneySendwhether the transaction moves value

To see the guard's decisions, set NAUTILUS_DEBUG=sensitive (optionally NAUTILUS_DEBUG_TOPICS=nonce): it prints the nonce reads, the signed transactions of refused sends and each recovery decision. Signed transactions are sensitive, see Debugging. The chain.nonce.* warnings print at the default log level.

An unsupported network

ocean.js's ConfigHelper ships defaults for a set of chains, among them Pontus-X devnet (32456) and OP Sepolia (11155420). Check nautilus.getOceanConfig() to see what you got. On a chain it does not know, supply the addresses yourself.

Nautilus.create validates nftFactoryAddress, fixedRateExchangeAddress and dispenserAddress up front, so a missing one fails immediately with a message naming it — rather than halfway through a publish.

chainId cannot select another network: the chain always comes from the signer's provider, and a config.chainId that differs from it throws. Connect the signer to the RPC of the chain you want.

const  = {
  : 32456,
  : 'pontusx',
 
  // The ocean-node: metadata, provider services and the indexer.
  : 'https://ocean-node.dev.pontus-x.eu',
  // The blockchain RPC.
  : 'https://rpc.dev.pontus-x.eu',
 
  // Validated at construction.
  : '0xFdC4a5DEaCDfc6D82F66e894539461a269900E13',
  : '0x8372715D834d286c9aECE1AcD51Da5755B32D505',
  : '0x5461b629E01f72E0A468931A36e039Eea394f9eA',
 
  : '0xdF171F74a8d3f4e2A789A566Dce9Fa4945196112',
  : 'OCEAN',
  : '0x68C24FA5b2319C81b34f248d1f928601D2E5246B'
} satisfies <>

For paid compute, set escrow to the chain's EnterpriseEscrow contract, which is where paid jobs lock their funds. Pontus-X devnet's address data in ocean.js lists no escrow, so paid compute is refused there until you set it. On a chain whose address data has one, leave escrow out: nautilus funds the chain's EnterpriseEscrow (else its Escrow). See Escrow contract.

For the full set of fields, see the ocean.js Config.

Reading back the resolved config

Useful for checking what the defaults actually gave you:

const  = .()