diff --git a/packages/ootle-wallet-daemon-signer/src/daemon-stealth-factory.ts b/packages/ootle-wallet-daemon-signer/src/daemon-stealth-factory.ts new file mode 100644 index 0000000..3bf5ed6 --- /dev/null +++ b/packages/ootle-wallet-daemon-signer/src/daemon-stealth-factory.ts @@ -0,0 +1,125 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +import type { + AccountsCreateStealthTransferStatementResponse, + ComponentAddressOrName, + OotleAddress, + ResourceAddress, + StealthTransferStatement as BindingsStealthTransferStatement, + TransferOutput, +} from "@tari-project/ootle-ts-bindings"; +import type { WalletDaemonClient } from "@tari-project/wallet_jrpc_client"; +import type { StealthOutputStatementFactory, StealthTransferStatement } from "@tari-project/ootle"; + +export interface DaemonStealthFactoryOptions { + /** The wallet daemon JRPC client (obtain via `WalletDaemonSigner.getClient()`). */ + client: WalletDaemonClient; + /** The sender account that owns the stealth UTXOs to spend. */ + senderAccount: ComponentAddressOrName; + /** The resource to transfer (e.g. the Tari token). */ + resourceAddress: ResourceAddress; + /** The recipient's full Ootle address (bech32m-encoded, includes both owner and view key). */ + recipientAddress: OotleAddress; +} + +/** + * A `StealthOutputStatementFactory` backed by a wallet daemon's + * `accounts.create_stealth_transfer_statement` JRPC endpoint. + * + * The wallet daemon handles all cryptographic operations (DH-KDF, Pedersen + * commitments, range proofs, balance proofs) server-side. No WASM crypto + * is required on the client. + * + * ## Usage + * + * ```ts + * const signer = await WalletDaemonSigner.connect({ url: "http://localhost:18103" }); + * 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(); + * ``` + * + * ## Type mapping + * + * The wallet daemon returns statements in the canonical bindings format + * (`@tari-project/ootle-ts-bindings` `StealthTransferStatement`), which uses + * `inputs_statement` / `outputs_statement` / `balance_proof`. + * + * The `@tari-project/ootle` package currently defines a simplified + * `StealthTransferStatement` with `outputs` / `balanceProof`. These types + * are structurally incompatible. This factory returns the **bindings format** + * (cast to satisfy the interface) because that is the format the on-chain + * `deposit_stealth` method expects when deserializing the JSON payload. + * + * A follow-up PR should align the `@tari-project/ootle` types with the + * bindings to eliminate this cast. + */ +export class DaemonStealthFactory implements StealthOutputStatementFactory { + private readonly client: WalletDaemonClient; + private readonly senderAccount: ComponentAddressOrName; + private readonly resourceAddress: ResourceAddress; + private readonly recipientAddress: OotleAddress; + + constructor(options: DaemonStealthFactoryOptions) { + this.client = options.client; + this.senderAccount = options.senderAccount; + this.resourceAddress = options.resourceAddress; + this.recipientAddress = options.recipientAddress; + } + + /** + * Generates a stealth transfer statement by delegating to the wallet daemon. + * + * @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[], + ): Promise { + const outputs: TransferOutput[] = amounts.map((amount) => ({ + address: this.recipientAddress, + revealed_amount: 0, + blinded_amount: amount, + memo: null, + pay_to: "StealthPublicKey" as const, + })); + + const response = + await this.client.sendRequest( + "accounts.create_stealth_transfer_statement", + { + requests: [ + { + sender_account: this.senderAccount, + resource_address: this.resourceAddress, + input_selection: { Selection: "PreferConfidential" }, + outputs, + }, + ], + }, + ); + + if (!response.statements || response.statements.length === 0) { + throw new Error("Wallet daemon returned no stealth transfer statements"); + } + + // The daemon returns the canonical bindings format. We cast to the tari.js + // StealthTransferStatement type. See class-level JSDoc for the type mismatch + // discussion — the bindings format is what deposit_stealth actually expects. + return response.statements[0] as unknown as StealthTransferStatement; + } +} diff --git a/packages/ootle-wallet-daemon-signer/src/index.ts b/packages/ootle-wallet-daemon-signer/src/index.ts index 57ff9c0..e0fffd7 100644 --- a/packages/ootle-wallet-daemon-signer/src/index.ts +++ b/packages/ootle-wallet-daemon-signer/src/index.ts @@ -5,6 +5,22 @@ export { WalletDaemonSigner } from "./wallet-daemon-signer"; export type { WalletDaemonSignerOptions } from "./wallet-daemon-signer"; export { authenticate } from "./auth"; export type { AuthOptions } from "./auth"; +export { DaemonStealthFactory } from "./daemon-stealth-factory"; +export type { DaemonStealthFactoryOptions } from "./daemon-stealth-factory"; // Re-export from @tari-project/wallet_jrpc_client so consumers don't need a direct dependency export { WalletDaemonClient } from "@tari-project/wallet_jrpc_client"; + +// Re-export commonly used stealth types from bindings for convenience +export type { + StealthTransferRequest, + StealthTransferResponse, + StealthUtxosListRequest, + StealthUtxosListResponse, + StealthUtxosDecryptValueRequest, + StealthUtxosDecryptValueResponse, + AccountsAssociateStealthResourceRequest, + AccountsAssociateStealthResourceResponse, + AccountsCreateStealthTransferStatementRequest, + AccountsCreateStealthTransferStatementResponse, +} from "@tari-project/ootle-ts-bindings"; diff --git a/packages/ootle-wallet-daemon-signer/src/wallet-daemon-signer.ts b/packages/ootle-wallet-daemon-signer/src/wallet-daemon-signer.ts index c3537db..da79415 100644 --- a/packages/ootle-wallet-daemon-signer/src/wallet-daemon-signer.ts +++ b/packages/ootle-wallet-daemon-signer/src/wallet-daemon-signer.ts @@ -1,7 +1,20 @@ // Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -import type { TransactionSignature, UnsignedTransactionV1 } from "@tari-project/ootle-ts-bindings"; +import type { + TransactionSignature, + UnsignedTransactionV1, + AccountsAssociateStealthResourceRequest, + AccountsAssociateStealthResourceResponse, + AccountsCreateStealthTransferStatementRequest, + AccountsCreateStealthTransferStatementResponse, + StealthTransferRequest, + StealthTransferResponse, + StealthUtxosListRequest, + StealthUtxosListResponse, + StealthUtxosDecryptValueRequest, + StealthUtxosDecryptValueResponse, +} from "@tari-project/ootle-ts-bindings"; import { WalletDaemonClient } from "@tari-project/wallet_jrpc_client"; import { type Signer, fromHexStr } from "@tari-project/ootle"; import { authenticate, type AuthOptions } from "./auth"; @@ -102,4 +115,77 @@ export class WalletDaemonSigner implements Signer { this._publicKey = fromHexStr(response.account.owner_public_key); this._address = response.address; } + + /** + * Returns the underlying `WalletDaemonClient` for advanced JRPC operations + * not covered by the signer's high-level API. + */ + public getClient(): WalletDaemonClient { + return this.client; + } + + // --------------------------------------------------------------------------- + // Stealth transfer operations + // --------------------------------------------------------------------------- + + /** + * Performs a stealth transfer in one call. The wallet daemon handles statement + * generation, transaction building, signing, and submission internally. + * + * This is the simplest path to a working stealth transfer when a wallet daemon + * is available. + */ + public async stealthTransfer( + params: StealthTransferRequest, + ): Promise { + return this.client.stealthTransfer(params); + } + + /** + * Generates a stealth transfer statement without building or submitting the + * transaction. The returned statement can be used with `TransactionBuilder` + * or `StealthTransfer` for custom transaction construction. + * + * Note: The `WalletDaemonClient` does not yet expose a typed method for this + * endpoint, so we use `sendRequest` directly. + */ + public async createStealthTransferStatement( + params: AccountsCreateStealthTransferStatementRequest, + ): Promise { + return this.client.sendRequest( + "accounts.create_stealth_transfer_statement", + params, + ); + } + + /** + * Associates a resource address with stealth tracking for an account. + * Must be called before the wallet daemon can list or manage stealth UTXOs + * for the given resource. + */ + public async associateStealthResource( + params: AccountsAssociateStealthResourceRequest, + ): Promise { + return this.client.accountsAssociateStealthResource(params); + } + + /** + * Lists stealth UTXOs known to the wallet daemon, optionally filtered + * by account and output status. + */ + public async stealthUtxosList( + params: StealthUtxosListRequest, + ): Promise { + return this.client.stealthUtxosList(params); + } + + /** + * Decrypts the blinded value of stealth UTXOs using the wallet daemon's + * view key. Returns the decrypted value for each requested UTXO. + */ + public async stealthUtxosDecryptValue( + params: StealthUtxosDecryptValueRequest, + ): Promise { + return this.client.stealthUtxosDecryptValue(params); + } }