From 7b2439dda0206bc3c4ae5fb59d79eefde97efd1e Mon Sep 17 00:00:00 2001 From: Marc Platt Date: Tue, 9 Jun 2026 11:52:13 -0400 Subject: [PATCH 1/2] mainchain/wallet: add BIP47 v1/v3 + BIP352 silent payment RPCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 11 new RPCs and their request/response types to WalletService for reusable payment protocols: BIP47 (v1 + v3 reusable payment codes) GetBip47PaymentCode — derive own payment code at m/47'/coin'/0' SendToBip47PaymentCode — send (broadcasts notification tx on first interaction per BIP47 mandate) ListBip47InboundPayers — senders who have notified us on-chain BIP352 (Silent Payments) GetSilentPaymentAddress — own sp1q… / tsp1q… address (with optional label index) CreateSilentPaymentLabel — allocate next m >= 1, persist, auto-rescan ListSilentPaymentLabels — known labels including m=0 change SendToSilentPayment — send to one or more SP addresses in one tx ListSilentPaymentReceives — detected incoming SP outputs Shared scanner controls GetReusableScanStatus — scanner cursor + tip RescanReusablePayments — force rescan from height --- proto/cusf/mainchain/v1/wallet.proto | 166 +++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/proto/cusf/mainchain/v1/wallet.proto b/proto/cusf/mainchain/v1/wallet.proto index ac00771..cdf6431 100644 --- a/proto/cusf/mainchain/v1/wallet.proto +++ b/proto/cusf/mainchain/v1/wallet.proto @@ -65,6 +65,34 @@ service WalletService { } // Available on regtest and signet only. rpc GenerateBlocks(GenerateBlocksRequest) returns (stream GenerateBlocksResponse); + + rpc GetBip47PaymentCode(GetBip47PaymentCodeRequest) returns (GetBip47PaymentCodeResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + rpc SendToBip47PaymentCode(SendToBip47PaymentCodeRequest) returns (SendToBip47PaymentCodeResponse); + rpc ListBip47InboundPayers(ListBip47InboundPayersRequest) returns (ListBip47InboundPayersResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + rpc GetSilentPaymentAddress(GetSilentPaymentAddressRequest) returns (GetSilentPaymentAddressResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + // Also rescans from the wallet birthday, so that past payments to the + // new label surface. + rpc CreateSilentPaymentLabel(CreateSilentPaymentLabelRequest) returns (CreateSilentPaymentLabelResponse); + rpc ListSilentPaymentLabels(ListSilentPaymentLabelsRequest) returns (ListSilentPaymentLabelsResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + rpc SendToSilentPayment(SendToSilentPaymentRequest) returns (SendToSilentPaymentResponse); + rpc ListSilentPaymentReceives(ListSilentPaymentReceivesRequest) returns (ListSilentPaymentReceivesResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + rpc GetReusableScanStatus(GetReusableScanStatusRequest) returns (GetReusableScanStatusResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + // Returns immediately; observe progress via GetReusableScanStatus. + rpc RescanReusablePayments(RescanReusablePaymentsRequest) returns (RescanReusablePaymentsResponse) { + option idempotency_level = IDEMPOTENT; + } } message BroadcastWithdrawalBundleRequest { @@ -283,3 +311,141 @@ message ListUnspentOutputsResponse { repeated Output outputs = 1; } + +// buf:lint:ignore ENUM_VALUE_PREFIX +enum Bip47Version { + BIP47_UNSPECIFIED = 0; + BIP47_V1 = 1; + BIP47_V3 = 3; +} + +message Bip47InboundPayer { + string payment_code = 1; + Bip47Version version = 2; + uint32 next_receive_index = 3; + uint64 total_received_sats = 4; + + // Unix timestamp of the notification tx we first observed. + int64 first_seen_unix = 5; +} + +message SilentPaymentLabel { + // Label index. m=0 is reserved for change. + uint32 m = 1; + + string name = 2; + string address = 3; +} + +message SilentPaymentReceive { + cusf.common.v1.ReverseHex txid = 1; + uint32 vout = 2; + + // X-only taproot output pubkey, 32 bytes. + bytes output_pubkey = 3; + + uint64 amount_sats = 4; + uint32 tweak_k = 5; + + // Set when the output was paid to a labeled address. Absent = + // base address; 0 = change; >=1 = user label. + optional uint32 label_m = 6; + optional string label_name = 7; + + uint32 height = 8; + + // Set if we have observed an on-chain spend of this UTXO. + optional cusf.common.v1.ReverseHex spent_in_txid = 9; +} + +message GetBip47PaymentCodeRequest { + Bip47Version version = 1; +} +message GetBip47PaymentCodeResponse { + string payment_code = 1; + + // P2PKH address that notification txs to us pay to. + string notification_address = 2; + + Bip47Version version = 3; +} + +message SendToBip47PaymentCodeRequest { + string payment_code = 1; + uint64 amount_sats = 2; + uint64 fee_sat_per_vbyte = 3; +} +message SendToBip47PaymentCodeResponse { + // Set on the first send to a given recipient; unset on subsequent sends + // (per BIP47, notification happens only on first interaction). + optional cusf.common.v1.ReverseHex notification_txid = 1; + + cusf.common.v1.ReverseHex payment_txid = 2; + uint32 sender_index = 3; + Bip47Version version = 4; +} + +message ListBip47InboundPayersRequest {} +message ListBip47InboundPayersResponse { + repeated Bip47InboundPayer payers = 1; +} + +message GetSilentPaymentAddressRequest { + // If set, return the labeled address instead of the base address. + optional uint32 label = 1; +} +message GetSilentPaymentAddressResponse { + string address = 1; +} + +message CreateSilentPaymentLabelRequest { + string name = 1; +} +message CreateSilentPaymentLabelResponse { + uint32 label_m = 1; + string labeled_address = 2; +} + +message ListSilentPaymentLabelsRequest {} +message ListSilentPaymentLabelsResponse { + repeated SilentPaymentLabel labels = 1; +} + +message SendToSilentPaymentRequest { + message Recipient { + string sp_address = 1; + uint64 amount_sats = 2; + } + repeated Recipient recipients = 1; + uint64 fee_sat_per_vbyte = 2; +} +message SendToSilentPaymentResponse { + cusf.common.v1.ReverseHex txid = 1; +} + +message ListSilentPaymentReceivesRequest { + optional uint32 min_confirmations = 1; + + // If not set, the server may apply a default ceiling. + optional uint32 limit = 2; +} +message ListSilentPaymentReceivesResponse { + repeated SilentPaymentReceive items = 1; + uint32 scan_tip_height = 2; +} + +message GetReusableScanStatusRequest {} +message GetReusableScanStatusResponse { + uint32 tip_height = 1; + uint32 last_scanned_height = 2; + uint32 birthday_height = 3; + bool catching_up = 4; +} + +message RescanReusablePaymentsRequest { + // All matches at or above this height are dropped before rescanning. + uint32 from_height = 1; +} +message RescanReusablePaymentsResponse { + uint32 scheduled_from_height = 1; +} From 594789a953a119dcd1abfdfe612c5fc92a2cf8f3 Mon Sep 17 00:00:00 2001 From: Marc Platt Date: Wed, 10 Jun 2026 01:24:29 -0400 Subject: [PATCH 2/2] mainchain/wallet: prefix Bip47Version enum values; make first_seen a google.protobuf.Timestamp --- proto/cusf/mainchain/v1/wallet.proto | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/proto/cusf/mainchain/v1/wallet.proto b/proto/cusf/mainchain/v1/wallet.proto index cdf6431..566a5bb 100644 --- a/proto/cusf/mainchain/v1/wallet.proto +++ b/proto/cusf/mainchain/v1/wallet.proto @@ -312,11 +312,10 @@ message ListUnspentOutputsResponse { repeated Output outputs = 1; } -// buf:lint:ignore ENUM_VALUE_PREFIX enum Bip47Version { - BIP47_UNSPECIFIED = 0; - BIP47_V1 = 1; - BIP47_V3 = 3; + BIP47_VERSION_UNSPECIFIED = 0; + BIP47_VERSION_V1 = 1; + BIP47_VERSION_V3 = 3; } message Bip47InboundPayer { @@ -325,8 +324,8 @@ message Bip47InboundPayer { uint32 next_receive_index = 3; uint64 total_received_sats = 4; - // Unix timestamp of the notification tx we first observed. - int64 first_seen_unix = 5; + // When we first observed the notification tx. + google.protobuf.Timestamp first_seen = 5; } message SilentPaymentLabel {