Skip to content

feat(wallet-daemon-signer): add stealth transfer support#110

Open
metalaureate wants to merge 1 commit intotari-project:mainfrom
metalaureate:feat/wallet-daemon-stealth-support
Open

feat(wallet-daemon-signer): add stealth transfer support#110
metalaureate wants to merge 1 commit intotari-project:mainfrom
metalaureate:feat/wallet-daemon-stealth-support

Conversation

@metalaureate
Copy link
Copy Markdown

Summary

Add stealth transfer support to WalletDaemonSigner and introduce DaemonStealthFactory — a concrete implementation of StealthOutputStatementFactory that delegates cryptographic operations to the wallet daemon.

Problem

The StealthOutputStatementFactory interface exists in @tari-project/ootle but has zero implementations. The underlying crypto primitives (DH-KDF, Pedersen commitments, range proofs, balance proofs) are not available in ootle-wasm, making a pure-browser implementation impossible today.

However, the wallet daemon already implements all of this crypto server-side, and @tari-project/ootle-ts-bindings already defines the request/response types (AccountsCreateStealthTransferStatementRequest, etc.). The JRPC endpoint accounts.create_stealth_transfer_statement exists on the daemon but lacks a typed client method.

Changes

WalletDaemonSigner — 6 new methods

Method JRPC endpoint Purpose
stealthTransfer() accounts.stealth_transfer All-in-one: daemon builds, signs, submits
createStealthTransferStatement() accounts.create_stealth_transfer_statement Low-level: returns statement for custom tx building
associateStealthResource() accounts.associate_stealth_resource Register resource for stealth tracking
stealthUtxosList() stealth_utxos.list Query stealth UTXOs
stealthUtxosDecryptValue() stealth_utxos.decrypt_value Decrypt blinded UTXO values
getClient() Expose underlying WalletDaemonClient

DaemonStealthFactory (new)

Implements StealthOutputStatementFactory by calling accounts.create_stealth_transfer_statement. This enables the existing StealthTransfer builder to work with a wallet daemon backend:

const factory = new DaemonStealthFactory({
  client: signer.getClient(),
  senderAccount: { Name: "default" },
  resourceAddress: TARI_TOKEN,
  recipientAddress: "ootle1...",
});

const spec = await new StealthTransfer(network, factory)
  .from(sourceAccount, resourceAddress)
  .to(recipientPublicKeyHex, 1_000_000n)
  .feeFrom(feeAccount, 1000n)
  .build();

Re-exports

Commonly used stealth types from @tari-project/ootle-ts-bindings are re-exported for convenience.

Known issue: type mismatch

The StealthTransferStatement in @tari-project/ootle uses a simplified shape:

{ outputs: StealthOutput[], balanceProof: string }

The canonical bindings type uses:

{ inputs_statement, outputs_statement, balance_proof }

DaemonStealthFactory returns the bindings format (which deposit_stealth expects on-chain) with a type cast. A follow-up PR should align the @tari-project/ootle types with the bindings.

Motivation

This work is motivated by Cairn, a cryptographic trust infrastructure for investigative journalism that uses stealth transfers for unlinkable source submissions. The daemon-backed factory provides a working path while pure-WASM stealth crypto is developed.

Add stealth transfer methods to WalletDaemonSigner:
- stealthTransfer(): all-in-one stealth transfer via daemon
- createStealthTransferStatement(): low-level statement generation
- associateStealthResource(): register stealth resource tracking
- stealthUtxosList(): query stealth UTXOs
- stealthUtxosDecryptValue(): decrypt blinded UTXO values
- getClient(): expose underlying WalletDaemonClient

Add DaemonStealthFactory implementing StealthOutputStatementFactory:
- Delegates crypto (DH-KDF, Pedersen commitments, range proofs,
  balance proofs) to the wallet daemon via JRPC
- Compatible with StealthTransfer builder for custom tx construction
- No WASM crypto required on the client side

Re-export commonly used stealth types from ootle-ts-bindings for convenience.

Note: The tari.js StealthTransferStatement type does not match the bindings
type. DaemonStealthFactory returns the bindings format (which is what the
on-chain deposit_stealth method expects) with a type cast. A follow-up PR
should align the ootle types with the canonical bindings types.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the DaemonStealthFactory class to facilitate stealth transfer statement generation via the wallet daemon and extends WalletDaemonSigner with high-level stealth operation methods. Feedback highlights a potential risk where the recipient public key parameter is ignored in favor of the factory's configured address, suggesting a validation check to prevent funds from being sent to the wrong destination. Additionally, it is recommended to make the hardcoded input_selection strategy configurable to provide users with more flexibility.

Comment on lines +83 to +91
* @param _recipientPublicKeyHex - Ignored in the daemon flow. The recipient is
* identified by the `recipientAddress` provided at construction time (which
* embeds the public key). This parameter is accepted to satisfy the
* `StealthOutputStatementFactory` interface.
* @param amounts - Amount(s) to send in each stealth output.
*/
public async generateOutputsStatement(
_recipientPublicKeyHex: string,
amounts: bigint[],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _recipientPublicKeyHex parameter is ignored in favor of the recipientAddress provided during construction. This creates a risk where a caller of the StealthTransfer builder might specify a different recipient via .to(key, amount) than the one this factory is configured for, leading to funds being sent to the wrong destination without any warning or error.

Since the StealthOutputStatementFactory interface only provides a hex public key, but the wallet daemon requires a full OotleAddress (which includes the view key), this factory is effectively pinned to a single recipient. You should consider adding a check to verify that _recipientPublicKeyHex matches the public key part of this.recipientAddress (if possible to decode) or at least document this limitation more explicitly in the method body to prevent accidental misuse.

{
sender_account: this.senderAccount,
resource_address: this.resourceAddress,
input_selection: { Selection: "PreferConfidential" },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The input_selection strategy is hardcoded to PreferConfidential. While this is a sensible default for stealth transfers, it limits flexibility for users who may need to specify a different selection policy (e.g., OnlyConfidential). Consider making this configurable via DaemonStealthFactoryOptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant