From 5971ec5d6f96fbff976bae50b8ead80bb75148e5 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 15 Jul 2026 01:29:02 +0200 Subject: [PATCH 1/3] docs: define transaction and mempool behavior --- .../docs/developer/integrations/http-api.md | 24 +++- .../developer/integrations/tx-lifecycle.md | 113 +++++++++--------- 2 files changed, 72 insertions(+), 65 deletions(-) diff --git a/src/content/docs/developer/integrations/http-api.md b/src/content/docs/developer/integrations/http-api.md index 1a2ff1d5..99f71b49 100644 --- a/src/content/docs/developer/integrations/http-api.md +++ b/src/content/docs/developer/integrations/http-api.md @@ -3,8 +3,8 @@ title: HTTP API description: Query chain data, call contracts, broadcast transactions, and subscribe to node events over HTTP and WebSocket. --- -:::tip[Prefer W3sper First] -For application integrations, prefer the [W3sper SDK](/developer/integrations/w3sper). It wraps transaction building, proving, submission, and common node interactions. +:::tip[Choose the right layer] +Use the [W3sper SDK](/developer/integrations/w3sper) for direct JavaScript node access and lower-level transaction primitives. W3sper is not a complete headless wallet: an automated signer must own protected keys and synchronized account or note state. Use this page for raw or non-JavaScript integrations. ::: Dusk nodes expose two main low-level HTTP surfaces: @@ -93,6 +93,16 @@ curl -s -X POST "https://nodes.dusk.network/graphql" \ }' | jq . ``` +### Query example: local mempool + +```sh +curl -s -X POST "https://nodes.dusk.network/graphql" \ + -H "Content-Type: application/json" \ + --data-raw '{"query":"{ mempoolTxs { id txType gasPrice gasLimit } }"}' | jq . +``` + +`mempoolTxs` returns the node's real local mempool in descending gas-price order. It is not a network-wide view, and it excludes future-nonce transactions temporarily staged in the node's prequeue. See [Transaction lifecycle](/developer/integrations/tx-lifecycle/#mempool-behavior) for admission, replacement, and expiry behavior. + ### Legacy raw-query endpoint `POST /on/graphql/query` is the legacy RUES GraphQL route. It accepts a raw GraphQL query string, and an empty body returns the schema SDL. @@ -210,6 +220,8 @@ curl -s -X POST "https://nodes.dusk.network/on/blocks/gas-price" | jq . For `preverify`, `propagate`, and `simulate`, send the serialized transaction as bytes, for example as a `0x...` hex string. +`propagate` returns `202 Accepted` after the node accepts the transaction for routing. This does not prove that it entered the real mempool, propagated to peers, executed successfully, or finalized. A valid future-nonce transaction can be staged outside the visible mempool while the node waits for the nonce gap to close. + Example format: ```sh @@ -245,7 +257,7 @@ Response: The deprecated account shortcut also returned `next_nonce`. That value was derived from pending mempool transactions and has no exact one-call successor. -For most clients, use `Number(nonce) + 1`. If you need mempool-aware nonce allocation, derive it client-side using GraphQL mempool data. +Use `Number(nonce) + 1` only when the account has no pending transactions. A signing service must serialize nonce allocation, retain its submitted transactions, and reconcile both local mempool and committed account state before reusing a nonce. ### Contracts @@ -413,7 +425,7 @@ The node sends the **session ID as the first WebSocket text message**. Use that Common topics: - `blocks`: `accepted`, `statechange`, `reverted` -- `transactions`: `included`, `removed`, `executed` +- `transactions`: `deferred`, `dropped`, `included`, `removed`, `executed` - `contracts`: contract-specific event topics, usually emitted event names Examples: @@ -449,6 +461,6 @@ Events are sent as WebSocket **binary messages**: 2. JSON header bytes, including `Content-Location`. 3. Raw event data bytes. The format depends on the event. -If you need historical data, use an archive-enabled node and GraphQL queries such as `moonlightHistory`, `fullMoonlightHistory`, and `finalizedEvents`. +If you need historical data, use an archive-enabled node and GraphQL queries such as `moonlightHistory`, `fullMoonlightHistory`, and `finalizedEvents`. Moonlight history is populated from finalized blocks, so it is suitable for restart-safe deposit processing without reconstructing finality from live events. -See: [Retrieve historical events](/developer/integrations/historical_events). +See [Scan Moonlight deposits](/developer/integrations/historical_events/) and [Transaction lifecycle](/developer/integrations/tx-lifecycle/). diff --git a/src/content/docs/developer/integrations/tx-lifecycle.md b/src/content/docs/developer/integrations/tx-lifecycle.md index 20a4cda0..f8c9633f 100644 --- a/src/content/docs/developer/integrations/tx-lifecycle.md +++ b/src/content/docs/developer/integrations/tx-lifecycle.md @@ -1,81 +1,76 @@ --- -title: Transaction Lifecycle -description: The lifecycle of a transaction on Dusk from creation to finalization +title: Transaction lifecycle +description: Transaction families, mempool behavior, execution, and finality on the Dusk L1. --- -:::note[General rule] -Transactions become immutable and are ultimately permanently recorded on the blockchain once a transaction is executed and the block reaches finality. -::: -## Steps executed during a transaction for Dusk +This page describes transactions submitted to the Dusk L1. DuskEVM has a separate [sequencer and finality model](/developer/duskevm/reference/#inclusion-and-finality). +## Transaction shape -Transactions on Dusk follow a specific lifecycle. Here's a basic overview: +A Dusk L1 transaction combines a value model with an optional action: -1. **Creation**: The process begins when a wallet or similar software generates a new transaction. -2. **Broadcasting**: The transaction is sent out to the Dusk network and broadcast within it. -3. **Validation**: Each node receiving the transaction verifies its validity before adding it to the Mempool. -4. **Inclusion in Mempool**: The transaction is added to the Mempool (*transaction included* event). - - If the transaction expires before being added to a block, it is removed from the Mempool (*transaction removed* event). - - If the transaction is replaced by another transaction with higher gas price, it is removed from the Mempool (*transaction removed* event) -5. **Inclusion in candidate block**: A block generator includes the transaction from the Mempool into a candidate block. - - If the transaction is discarded during the block generation, it is removed from the Mempool (this event is currently not emitted[^1]) -6. **Acceptance**: The block containing the transaction is accepted into the blockchain (*block accepted* event). - 1. **Execution**: When accepting the block, the transaction is executed (*transaction executed* event). - - **Successful Transaction**: No errors available - - **Reverted or Failed Transaction**: Errors available (*transaction executed* event with error field available). This is contract-specific and indicates a revert (panic) or other error that was returned. - 2. **Removal from Mempool**: The transaction is removed from the mempool (*transaction removed* event). -7. **Confirmation**: The block is confirmed, making the transaction unlikely to be reverted (*block state-change* event with state changed to `confirmed`). -8. **Finalization**: The block reaches finality, making the transaction immutable and irreversible. (*block state-change* event with state changed to `finalized`). +| Layer | Options | Operational effect | +|---|---|---| +| Value model | **Moonlight** or **Phoenix** | Moonlight uses public accounts and sequential nonces. Phoenix uses shielded notes and nullifiers. | +| Action | Transfer, memo, contract call, contract deployment, or blob | The action is carried by either transaction family. A contract call can also deposit DUSK into the called contract. | -:::note[Failed Transactions] -Failed transactions are defined here as a concept at the contract level. We assume that each contract, just like the transfer contract or other genesis contracts, makes use of proper error handling and panics: +Moonlight exposes its sender, optional receiver, value, nonce, fee, and optional memo. Phoenix hides transferred values and participants while exposing the cryptographic data needed to verify note spends. -- If a contract panics, the transaction is reverted and there will be an error to look at. -- If a contract returns a result that contains an error, there will also be an error to look at. +A memo can accompany a direct transfer and is limited to 512 bytes. The transaction data field holds one of a memo, contract call, deployment, or blob payload; do not assume a memo can accompany the other payload types. -Both of these are **contract specific**, the chain still executed the transaction successfully - according to the code of the smart contract. +## Lifecycle -A failed or reversed transaction has nothing to do with Dusk itself directly. The transaction was successfully executed according to Dusk's rules and recorded on the blockchain, hence the transaction executed event. -::: +1. **Build and sign:** the client chooses the transaction family, action, gas limit, gas price, and current spend inputs or nonce. +2. **Submit:** the client sends serialized bytes to a node. `POST /on/transactions/propagate` returning `202 Accepted` means the node accepted the request for routing; it does not mean the transaction is in a block. +3. **Admit:** the node pre-verifies the transaction. A valid transaction enters that node's real mempool and emits `transactions/included`. +4. **Propagate:** the receiving node broadcasts an admitted transaction to peers. Each peer performs its own admission checks and maintains its own mempool. +5. **Select:** a block generator considers mempool transactions in descending gas-price order, subject to block limits and execution dependencies. +6. **Execute:** an accepted block emits `transactions/executed` for each spent transaction. `err: null` means execution succeeded; a non-null `err` records a contract panic or other execution failure. Either result consumes the nonce or notes and pays gas. +7. **Finalize:** accepted blocks can still be reverted. A `blocks/statechange` event with `state: "finalized"` makes the block and its transactions final. -:::caution[Reverted Blocks] -Before reaching finality, blocks sometimes revert according to consensus rules. +Do not use **included**, **removed**, **accepted**, or **confirmed** as a payment-finality signal. -This can happen during steps **6** and **7**, (block reverted event). In such cases, you need to re-listen for transaction events to get the required information. -::: +## Mempool behavior -### Checking transaction status -> This applies to all genesis contracts and any future third party contracts that use proper error handling and panics. -- **Successful Transaction**: Check for the combination of a successful transaction executed event (with error `None`) **and** the finalized block event. - - `transaction executed` event with no errors and `block statechange` `finalized` event. -- **Reverted & Failed Transactions**: Check the transaction executed event for errors. - - `transaction executed` event with error field available. -- **Reverted Block**: If a block is reverted, re-listen for the transaction events and the new block. - - `block reverted` event and re-evaluate the transaction status. +| Question | Current behavior | +|---|---| +| Who can inspect it? | Anyone with GraphQL access to a node can query that node's real mempool through `mempoolTxs` or `mempoolTx`. This is a local view, not a network-wide snapshot. | +| How are transactions prioritized? | Candidate selection iterates by descending `gasPrice`. Equal-price ordering is not a client contract. | +| How does replacement work? | A transaction that conflicts on a spending ID replaces the existing transaction only when its gas price is strictly higher. Raising only the gas limit is insufficient. Moonlight uses account and nonce as its spending ID; Phoenix conflicts on spent nullifiers. | +| What happens when the mempool is full? | A higher-priced transaction can evict the lowest-priced entry. Node operators configure the capacity; the default is 10,000 transactions. | +| When do transactions expire? | Expiry is local node policy, not a field in the transaction. The default residence is three days, checked hourly, and both values are configurable. Clients must not treat three days as a network guarantee. | +| What happens to future Moonlight nonces? | A transaction with a nonce gap is briefly staged outside the real mempool while the node waits for intermediate nonces. It emits `deferred`, is invisible to `mempoolTxs`, and is admitted only if the gap is filled before retries end. | +| Can every node publish transactions? | A full Rusk node can receive and rebroadcast transactions. Its operator can disable or restrict public HTTP access, apply ACLs, or rate-limit propagation. | -:::tip[Info] -For information about events you can look at the [Rusk Universal Event System (RUES)](/developer/integrations/http-api) page -::: +`transactions/removed` only says that an entry left the local mempool. Inclusion in a block, replacement, expiry, capacity eviction, or removal of a conflicting transaction can all cause it. Query ledger state before deciding what happened. -### Practical considerations for listening to transactions -For example for handling Dusk deposits with Moonlight: -- Monitor the transaction executed event and ensure no errors. - - Check for relevant contract specific events like `MoonlightTransactionEvent` -- Confirm that the block containing the transaction is finalized. -- Handle block reverts by re-listening for transaction events. +## Events -By following these guidelines, you can ensure accurate tracking of transactions on the Dusk blockchain. +RUES exposes these transaction topics: -### Deep dive: Discarded transactions +| Topic | Meaning | +|---|---| +| `deferred` | Staged outside the real mempool because an admission prerequisite is missing | +| `dropped` | Discarded before entering the real mempool | +| `included` | Added to the local real mempool | +| `removed` | Removed from the local real mempool for any reason | +| `executed` | Executed in an accepted block; inspect `err` | -This is nothing to be concerned about and can be ignored if you are running official wallet software or using the official SDKs (eg. w3sper). +Block topics are `accepted`, `statechange`, and `reverted`. See the [HTTP API event model](/developer/integrations/http-api/#event-subscriptions) for subscription details. -A transaction is rarely discarded in **two** cases: -- Invalid to protocol specifications (payload incomplete, corrupted, wrong) -- Gas limit is below the protocol minimum gas limit +## Confirm success -Such invalid transactions are also caught by pre-verifications on multiple steps both in the wallet and later on the node. +For a live transaction watcher: -To achieve this, one must actively use a modified or incorrectly implemented wallet software or SDK. Such transactions can be compared to invalid data packets on a port that are simply ignored because the listening application has no use for them. +1. Query `tx(hash: ...)` after execution and require `err` to be null. +2. Retain its `blockHeight` and `blockHash`. +3. On an archive node, require `checkBlock(height: ..., hash: ..., onlyFinalized: true)` to return `true`. +4. Store the transaction ID as an idempotency key. -[^1]: The emission of this event is planned to be implemented soon. \ No newline at end of file +Archive Moonlight history is populated only when blocks finalize. Deposit systems can therefore use the [finalized deposit scanner](/developer/integrations/historical_events/) instead of reconstructing finality from live events. + +## Implementation references + +- [Mempool admission and replacement](https://github.com/dusk-network/rusk/tree/master/node/src/mempool) +- [GraphQL transaction and mempool queries](https://github.com/dusk-network/rusk/blob/master/rusk/src/lib/http/chain/graphql/tx.rs) +- [Transaction and block event definitions](https://github.com/dusk-network/rusk/tree/master/node-data/src/events) From 40b2e1eb65aa79061a4a6d0d0103feee869a0b76 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 15 Jul 2026 01:29:02 +0200 Subject: [PATCH 2/3] docs: add finalized Moonlight deposit workflow --- .../docs/developer/integrations/exchanges.md | 120 ++++----- .../integrations/historical_events.md | 240 +++++++----------- src/sidebars/siteSidebar.js | 2 +- 3 files changed, 142 insertions(+), 220 deletions(-) diff --git a/src/content/docs/developer/integrations/exchanges.md b/src/content/docs/developer/integrations/exchanges.md index 0163e9fb..76038379 100644 --- a/src/content/docs/developer/integrations/exchanges.md +++ b/src/content/docs/developer/integrations/exchanges.md @@ -1,97 +1,77 @@ --- -title: Integrate with Exchanges -description: "Practical notes for listing DUSK: connectivity, deposits/withdrawals, and references." +title: Exchange integration +description: Operate finalized Moonlight deposits and controlled DUSK withdrawals. --- -## Integration Checklist +Exchange integrations should use Moonlight, Dusk's public account model. Phoenix addresses and shielded notes require a different custody and scanning model; users can move funds to a public account before depositing. -- Decide how you'll access the network: - - Run your own node (recommended for production). - - Use public endpoints (good for prototyping). - - Run (or use) an archive node if you need address-based history queries such as `fullMoonlightHistory` or `moonlightHistory`. -- Support Moonlight (public) deposits and withdrawals. -- Handle finality and reverts: treat funds as final once the containing block is `finalized`, and re-process if a block is reverted. -- Use the memo field if you need per-user tagging. +## Recommended architecture -:::tip[Use W3sper for Transaction Submission] -For transaction construction, signing, and submission, prefer the [W3sper SDK](/developer/integrations/w3sper). Use raw HTTP endpoints only if you are building lower-level infrastructure around RUES. -::: +| Component | Responsibility | +|---|---| +| Archive Rusk node | Supplies finalized Moonlight history and deterministic backfills | +| Deposit scanner | Filters accepted deposit types, writes idempotent credits, and advances block checkpoints | +| Custody ledger | Maps accounts or memos to customers and reconciles on-chain balances | +| Signing service | Protects keys, serializes Moonlight nonces, builds transactions, and stores signed bytes before submission | +| Broadcast node | Pre-verifies, propagates, and reports local mempool and ledger state | -## Network Access +Run nodes you control for production. Public endpoints are suitable for development but are not a substitute for your own availability, retention, and access policy. -Base URLs: +## Deposits -- **Mainnet:** `https://nodes.dusk.network` -- **Testnet:** `https://testnet.nodes.dusk.network` +Choose one attribution model: -GraphQL endpoint: +- **Account per customer:** scan each assigned Moonlight account. +- **Shared account with memo:** scan the shared account, decode the memo from hex, and accept only your documented memo format. -- `POST /on/graphql/query` +Use the [Moonlight deposit scanner](/developer/integrations/historical_events/) against finalized archive history. It deliberately accepts direct Moonlight transfers only. Add contract payouts or Phoenix conversions only through separate, explicit event rules. -For the full HTTP/WS API and event subscription model (RUES), see [/developer/integrations/http-api](/developer/integrations/http-api). +For every credit: -## Monitoring Deposits +- store amounts as integer LUX (`1 DUSK = 1_000_000_000 LUX`); +- use the Dusk transaction ID as the idempotency key; +- quarantine missing, malformed, unknown, or reused customer metadata; and +- update credits and the block checkpoint atomically. -Two common approaches: +Do not credit from a balance change, local mempool entry, `transactions/included`, or an accepted but unfinalized block. -- **Archive GraphQL (simplest):** poll `fullMoonlightHistory` or `moonlightHistory` on an archive-enabled node. See [/developer/integrations/historical_events](/developer/integrations/historical_events). -- **Real-time (RUES):** subscribe to blocks/transactions and correlate with GraphQL `tx(hash: ...)` lookups. +## Withdrawals -## Submitting Withdrawals +W3sper provides transaction builders and submission primitives, not a complete headless wallet. A signing service must supply protected recoverable keys and synchronized `Bookkeeper` state, including committed balances, pending transactions, and Moonlight nonces. -- Use the [W3sper SDK](/developer/integrations/w3sper) to construct, sign, and broadcast transactions. -- Or broadcast raw bytes via `POST /on/transactions/propagate` (see the HTTP API page). +For each withdrawal: -## GraphQL Quick Queries +1. Assign an internal withdrawal ID and reserve the next nonce atomically for the hot account. +2. Build and sign once, then persist the exact serialized transaction and its Dusk transaction ID before broadcasting. +3. Submit through W3sper or `POST /on/transactions/propagate`. +4. Treat `202 Accepted` only as successful routing. Query execution and finality before marking the withdrawal complete. +5. On a transport timeout, rebroadcast the same signed bytes instead of creating a new transaction blindly. -Get the schema (SDL): +A same-nonce replacement must use a strictly higher gas price and has a different transaction ID. Reconcile both IDs and never credit or debit twice. See [Transaction lifecycle](/developer/integrations/tx-lifecycle/#mempool-behavior). -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" -``` +Use [Rusk Wallet](/use/wallets/#rusk-wallet) for operator-controlled manual transactions. For an automated signer, start from the [W3sper source and tests](https://github.com/dusk-network/rusk/tree/master/w3sper.js) and implement key storage, synchronization, nonce allocation, approval policy, and audit logging as owned infrastructure. -Transaction by hash: +## Network access -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { tx(hash: "") { tx { id gasLimit gasPrice memo } err gasSpent blockHash blockHeight blockTimestamp } }' -``` +| Network | Node base URL | Explorer | +|---|---|---| +| Mainnet | `https://nodes.dusk.network` | [explorer.dusk.network](https://explorer.dusk.network/) | +| Testnet | `https://testnet.nodes.dusk.network` | [apps.testnet.dusk.network/explorer](https://apps.testnet.dusk.network/explorer/) | -Latest block header: +GraphQL is available at `POST /graphql`. Transaction submission uses `POST /on/transactions/propagate`. See the [HTTP API](/developer/integrations/http-api/) for request formats and node policy controls. -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { block(height: -1) { header { height hash timestamp } } }' -``` +## Asset metadata -## Transaction Model Notes +| Field | Value | +|---|---| +| Asset | DUSK | +| Smallest unit | LUX | +| Native decimals | 9 | +| Public account model | Moonlight | +| Consensus | Succinct Attestation | -- Exchanges typically only need Moonlight (public) accounts. Phoenix (shielded) addresses are incompatible with Moonlight transfers. -- Users can convert shielded funds to public balances themselves before depositing to an exchange. +ERC20 and BEP20 representations use 18 decimals. Keep their metadata and accounting separate from native DUSK. Users moving legacy representations to Dusk mainnet should follow the [migration guide](/learn/guides/mainnet-migration/). -## Cold Storage +## Custody controls -The [multisig contract](https://github.com/dusk-network/multisig-contract) contains a working example of multi-signature transfers for cold storage. - -## Token Details - -- Token: `dusk` -- Token decimals: `9` (ERC20/BEP20 versions use 18 decimals) -- Consensus mechanism: Succinct Attestation - -## Token Migration (If Applicable) - -Users can migrate ERC20/BEP20 DUSK to DUSK on Dusk mainnet using the [migration contract](https://github.com/dusk-network/dusk-migration) and the [Web Wallet](https://apps.dusk.network/wallet/). - -More information: [/learn/guides/mainnet-migration](/learn/guides/mainnet-migration). - -Current token contracts: - -- ERC20: `0x940a2db1b7008b6c776d4faaca729d6d4a4aa551` -- BEP20: `0xb2bd0749dbe21f623d9baba856d3b0f0e1bfec9c` - -## Resources - -- [Whitepaper](https://dusk-cms.ams3.digitaloceanspaces.com/Dusk_Whitepaper_2024_4db72f92a1.pdf) -- [Audits](https://github.com/dusk-network/audits) -- [Block explorer](https://explorer.dusk.network/) +The [Dusk contract standards](https://github.com/dusk-network/contracts/tree/main/standards) include reusable multisig and access-control primitives. They are a starting point for Dusk-native custody policies, not a substitute for your own threat model, review, and operational controls. diff --git a/src/content/docs/developer/integrations/historical_events.md b/src/content/docs/developer/integrations/historical_events.md index d7aacea5..d1e1a548 100644 --- a/src/content/docs/developer/integrations/historical_events.md +++ b/src/content/docs/developer/integrations/historical_events.md @@ -1,189 +1,131 @@ --- -title: Retrieve historical events -description: Documentation on the access to blockchain transaction history for public value transfers on Dusk. +title: Scan Moonlight deposits +description: Detect finalized public DUSK deposits with archive GraphQL and W3sper. --- -## Overview +Use an archive-enabled Rusk node to scan public Moonlight deposits. The Moonlight history indexes are updated only when blocks finalize, so their results do not require a separate confirmation delay. -These queries are **archive-only**. You need an archive-enabled node, or a public archive endpoint, to use them. On a non-archive node they are unavailable. +The public endpoints are useful for development. Run an archive node you control for production ingestion, availability, and reproducible backfills. -The archive GraphQL API provides access to finalized historical blockchain transaction-events involving public (Moonlight) `DUSK` value transfers. It includes two primary endpoints: +## Choose a query -1. `moonlightHistory` -2. `fullMoonlightHistory` +| Query | Returns | +|---|---| +| `moonlightHistory(receiver: ...)` | Finalized transactions that caused an inflow to one public account, with optional block bounds and offset pagination | +| `fullMoonlightHistory(address: ...)` | Finalized inflows and outflows affecting one public account | +| `transactionsByMemo(memo: ...)` | Finalized indexed transactions carrying an exact memo | -:::tip -The **moonlightHistory** endpoint allows for advanced filtering, enabling queries based on sender, receiver, block range, and pagination. **fullMoonlightHistory** retrieves all public `DUSK` value transfers for a given address, without additional filtering options. -::: +History can include direct transfers, contract payouts, refunds, staking withdrawals, and Phoenix-to-Moonlight conversions. A direct Moonlight deposit is specifically a non-reverted transfer-contract event with topic `moonlight`, the expected receiver, and a positive value. -You can also check out the [transaction models](/learn/deep-dive/duskds-tx-models) page to get familiar with the terminology of `moonlight` and `phoenix`. +Do not identify direct transfers by counting events. A transaction can emit additional contract events without changing its transaction family. -Refer to [GraphQL queries](/developer/integrations/http-api/#graphql-queries) for additional information on the GraphQL endpoint. +## W3sper scanner -## Endpoints +Install W3sper: -### `moonlightHistory` - -Retrieves emitted events from transactions based on sender and/or receiver. The `moonlightHistory` endpoint allows you to specify block ranges (fromBlock, toBlock) to retrieve all transfers within the specified block range. You can also specify a maxCount and a pageCount to limit the number of results returned. Pagination should be used with care as it is offset-based and **not** cursor-based. - -#### Example Queries - -##### Query by Sender - -> If only **sender** is specified, it will only show moonlight outflows for that address. - -```graphql -query { - moonlightHistory(sender: "") { - json - } -} +```bash +deno add jsr:@dusk/w3sper ``` -**cURL Command:** - -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { moonlightHistory(sender: "") { json } }' -``` +Create `scan-deposits.js`: -##### Query by Receiver +```js +import { Network } from "@dusk/w3sper"; -> If only **receiver** is specified, it will only show moonlight inflows for that address. +const TRANSFER_CONTRACT = + "0100000000000000000000000000000000000000000000000000000000000000"; -```graphql -query { - moonlightHistory(receiver: "") { - json - } +const account = Deno.env.get("MOONLIGHT_ACCOUNT"); +if (!account) throw new Error("Set MOONLIGHT_ACCOUNT"); +if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(account)) { + throw new Error("MOONLIGHT_ACCOUNT must be Base58"); } -``` -**cURL Command:** +const nodeUrl = Deno.env.get("DUSK_NODE") ?? "https://nodes.dusk.network"; +const startHeight = Number(Deno.env.get("FROM_BLOCK") ?? "0"); +const batchSize = Number(Deno.env.get("BLOCK_BATCH_SIZE") ?? "1000"); -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { moonlightHistory(receiver: "") { json } }' -``` - - - -##### Query by Sender and Receiver - -> If **both sender and receiver** are specified, it will **only** return events from transactions where an outflow occurred on the sender's side and an inflow occurred on the receiver's side within the same transaction. - -```graphql -query { - moonlightHistory( - sender: "" - receiver: "" - ) { - json - } +if (!Number.isSafeInteger(startHeight) || startHeight < 0) { + throw new Error("FROM_BLOCK must be a non-negative integer"); } -``` - -**cURL Command:** - -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { moonlightHistory(sender: "", receiver: "") { json } }' -``` - -### `fullMoonlightHistory` - -Returns all transactions that have an inflow or outflow of public Dusk for the given address, without the granular filtering options available in `moonlightHistory`. The `fullMoonlightHistory` endpoint allows you to specify block ranges (fromBlock, toBlock) to retrieve all transfers within the specified block range. - -#### Example Query - -```graphql -query { - fullMoonlightHistory(address: "
") { - json - } +if (!Number.isSafeInteger(batchSize) || batchSize < 1) { + throw new Error("BLOCK_BATCH_SIZE must be a positive integer"); } -``` - -**cURL Command:** - -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { fullMoonlightHistory(address: "
") { json } }' -``` - -## Filtering - -### Filtering for public transactions - -There is a distinction between a **protocol transaction** and a **transfer of value**: -- Transactions can be of type `moonlight`, but some transactions can also be of type `phoenix` and they can still lead to a change in the public balance of an account, while not being a public (moonlight) transaction (E.g., in the case of a conversion from shielded dusk **to** public dusk). -- If you only want to see public transactions, you can filter the JSON response for transaction entries where at least one event contains the following fields: - ```json - "target": "0100000000000000000000000000000000000000000000000000000000000000", - "topic": "moonlight" - ``` +const network = await Network.connect(nodeUrl); -Example JavaScript, Rust, Bash function to filter based on it: +try { + const status = await network.query("lastBlockPair { json }"); + const finalizedHeight = status.lastBlockPair.json.last_finalized_block[0]; -#### JavaScript - -```javascript -function filterMoonlightTransactions(response) { - return response.fullMoonlightHistory.json.filter(tx => - tx.events.some(event => - event.target === "0100000000000000000000000000000000000000000000000000000000000000" && - event.topic === "moonlight" + for ( + let fromBlock = startHeight; + fromBlock <= finalizedHeight; + fromBlock += batchSize + ) { + const toBlock = Math.min( + fromBlock + batchSize - 1, + finalizedHeight, + ); + const result = await network.query(` + moonlightHistory( + receiver: "${account}" + fromBlock: ${fromBlock} + toBlock: ${toBlock} + ) { + json + } + `); + + const deposits = (result.moonlightHistory?.json ?? []).flatMap((group) => + group.events + .filter((event) => + event.target === TRANSFER_CONTRACT && + event.topic === "moonlight" && + event.reverted === false && + event.data?.receiver === account && + BigInt(event.data?.value ?? "0") > 0n ) + .map((event) => ({ + txId: group.origin, + blockHeight: group.block_height, + amountLux: event.data.value, + memoHex: event.data.memo || null, + })) ); -} -``` - -#### Rust -```rust -use serde_json::Value; - -fn filter_moonlight_transactions(response: &Value) -> Vec<&Value> { - response["fullMoonlightHistory"]["json"].as_array().unwrap_or(&vec![]).iter() - .filter(|tx| tx["events"].as_array().unwrap_or(&vec![]).iter() - .any(|event| event["target"] == "0100000000000000000000000000000000000000000000000000000000000000" - && event["topic"] == "moonlight")) - .collect() + console.log(JSON.stringify({ fromBlock, toBlock, deposits })); + } +} finally { + await network.disconnect(); } ``` -#### Bash -> using jq -```sh -jq '.fullMoonlightHistory.json | map(select(.events | any(.target == "0100000000000000000000000000000000000000000000000000000000000000" and .topic == "moonlight")))' +Run it from the first unprocessed block: + +```bash +MOONLIGHT_ACCOUNT='' \ +FROM_BLOCK='' \ +deno run --allow-env --allow-net scan-deposits.js ``` -**cURL Command with Filtering:** +Amounts are integer LUX strings: `1 DUSK = 1_000_000_000 LUX`. Memos are returned as hex and are untrusted routing data; validate their decoded format before mapping them to a customer. -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { fullMoonlightHistory(address: "
") { json } }' \ - | jq '.fullMoonlightHistory.json | map(select(.events | any(.target == "0100000000000000000000000000000000000000000000000000000000000000" and .topic == "moonlight")))' -``` +## Persist safely -This ensures that only public protocol transactions are retrieved. +For each completed block range, use one database transaction to: -### Filtering for public transaction & plain standard transfer +1. Insert credits with a unique constraint on the Dusk transaction ID. +2. Record unknown or invalid destination metadata for manual review instead of dropping it. +3. Advance the account's `next_block` checkpoint to `toBlock + 1`. +4. Commit only after all credits in the range are durable. -To retrieve only **standard** Moonlight transfers (i.e., transactions that involve a direct value transfer without additional actions, such as contract calls), you can filter for transactions where the events array contains exactly one entry. +After a crash, restart from `next_block`. Re-scanning a range is safe when transaction IDs are unique. Never advance the checkpoint before writing its deposits, and never use a memo as the idempotency key. -The entry we are looking for is a `MoonlightTransactionEvent`, which is the same as the one above, but it will be the only event emitted during that transaction. So the only additional constraint is that events have only one entry. +Use bounded block ranges instead of deep offset pagination. This keeps backfills predictable while new finalized blocks are added. -#### Example Bash -```sh -jq '.fullMoonlightHistory.json | map(select(.events | length == 1 and any(.target == "0100000000000000000000000000000000000000000000000000000000000000" and .topic == "moonlight")))' -``` +## Other public inflows -**cURL Command with Filtering:** +The scanner intentionally accepts only direct Moonlight transfers. If your integration also accepts contract payouts, staking withdrawals, refunds, or Phoenix conversions, define and test a separate rule for each relevant transfer-contract event such as `contract_to_account`, `mint`, `withdraw`, or `convert`. -```sh -curl -s -X POST "https://nodes.dusk.network/on/graphql/query" \ - --data-raw 'query { fullMoonlightHistory(address: "
") { json } }' \ - | jq '.fullMoonlightHistory.json | map(select(.events | length == 1 and any(.target == "0100000000000000000000000000000000000000000000000000000000000000" and .topic == "moonlight")))' -``` +See [Transaction lifecycle](/developer/integrations/tx-lifecycle/) for live mempool and finality behavior, and [HTTP API](/developer/integrations/http-api/#graphql-queries) for raw GraphQL access. diff --git a/src/sidebars/siteSidebar.js b/src/sidebars/siteSidebar.js index ba4823ad..656574ac 100644 --- a/src/sidebars/siteSidebar.js +++ b/src/sidebars/siteSidebar.js @@ -107,8 +107,8 @@ const siteSidebar = [ items: [ { label: "W3sper SDK", link: "/developer/integrations/w3sper" }, { label: "Transaction Lifecycle", link: "/developer/integrations/tx-lifecycle" }, + { label: "Deposit Scanning", link: "/developer/integrations/historical_events" }, { label: "HTTP API", link: "/developer/integrations/http-api" }, - { label: "Historical Events", link: "/developer/integrations/historical_events" }, { label: "Encoding and Hashing", link: "/developer/integrations/reference" }, { label: "Exchange Integration", link: "/developer/integrations/exchanges" }, ], From 1dcbb3c5e829ba63c04def1dc36c50489531fd44 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 15 Jul 2026 01:29:02 +0200 Subject: [PATCH 3/3] docs: match W3sper offline driver version --- src/content/docs/developer/integrations/w3sper.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/content/docs/developer/integrations/w3sper.md b/src/content/docs/developer/integrations/w3sper.md index df21a5ab..f6d76b5a 100644 --- a/src/content/docs/developer/integrations/w3sper.md +++ b/src/content/docs/developer/integrations/w3sper.md @@ -99,12 +99,12 @@ console.log(tip.block); ## Offline profile derivation -`Network.connect()` loads `wallet-core-1.6.1.wasm` from the selected node. An offline application can download that same driver, store it locally, and load it before using profile APIs: +`Network.connect()` loads the protocol driver expected by the installed W3sper release from the selected node. `@dusk/w3sper` 1.6.0 requests `wallet-core-1.6.0.wasm`. An offline application must download that exact driver, store it locally, and load it before using profile APIs: ```js import { ProfileGenerator, useAsProtocolDriver } from "@dusk/w3sper"; -const driver = await Deno.readFile("./wallet-core-1.6.1.wasm"); +const driver = await Deno.readFile("./wallet-core-1.6.0.wasm"); const seeder = () => crypto.getRandomValues(new Uint8Array(64)); await useAsProtocolDriver(driver); @@ -118,12 +118,15 @@ The random seed above is only an API demonstration. Production applications must Driver URLs: -- Mainnet: `https://nodes.dusk.network/static/drivers/wallet-core-1.6.1.wasm` -- Testnet: `https://testnet.nodes.dusk.network/static/drivers/wallet-core-1.6.1.wasm` +- Mainnet: `https://nodes.dusk.network/static/drivers/wallet-core-1.6.0.wasm` +- Testnet: `https://testnet.nodes.dusk.network/static/drivers/wallet-core-1.6.0.wasm` + +Check the installed W3sper release before pinning an offline driver; the filename changes when its protocol-driver dependency changes. ## Next steps - [Dusk Connect](/developer/integrations/dusk-connect/) - [DuskVM quickstart](/developer/duskvm/quickstart/) +- [Moonlight deposit scanning](/developer/integrations/historical_events/) - [Transaction lifecycle](/developer/integrations/tx-lifecycle/) - [HTTP API and RUES](/developer/integrations/http-api/)