Repository navigation
Conversation
Ports btc-mana-bar's Buzz client and config (BCOIN-10549) into a dedicated buzz package, so other services can post to a Buzz channel without copying it. - BuzzChannel.parse validates a community's https:// or wss:// origin and a lowercase channel UUID. - BuzzClient posts a kind-9 GroupChatMessage tagged with the channel to the community's /events endpoint, authorized by a NIP-98 HttpAuth event, and succeeds only when the relay acknowledges that event id. - EventContent.sign(sec, at) signs at a given instant, so a retry re-sends the same event. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a1ad78-8f9d-75ce-954c-6877855b49f2
|
🤖 @builderbot review |
rgrnwd
left a comment
There was a problem hiding this comment.
The client needs fixes for copied authorization fields and malformed acknowledgement handling.
| val payload: ByteString? = null, | ||
| val nonce: String? = null, | ||
| override val tags: List<Tag> = listOfNotNull( | ||
| UrlTag(url), |
There was a problem hiding this comment.
The generated data-class copy preserves existing tags, so copy(url = anotherUrl) or copy(payload = anotherHash) still signs the original URL or payload and produces authorization the new request cannot use. Derive required tags from the current fields, and add coverage for copying authorization fields; GroupChatMessage has the same problem with copy(groupId = ...).
| .build() | ||
| val acknowledgement = httpClient.newCall(request).execute().use { response -> | ||
| if (!response.isSuccessful) throw IOException("Buzz relay returned HTTP ${response.code}.") | ||
| acknowledgementAdapter.fromJson(response.body.source()) |
There was a problem hiding this comment.
A successful HTTP response containing an unexpected JSON shape, such as [], makes Moshi throw JsonDataException instead of the documented IOException, bypassing callers' IOException handling for failed posts. Wrap decoding failures in IOException and test malformed acknowledgements.
| /** Accepts every event, acknowledging it by its own id as a Buzz relay does. */ | ||
| val acknowledging = object : Dispatcher() { | ||
| override fun dispatch(request: RecordedRequest): MockResponse { | ||
| val id = Event.fromJson(request.body.clone().readUtf8())!!.id.hex() |
jacksoncook
left a comment
There was a problem hiding this comment.
The new nonce tag handling loses data from NIP-13 proof-of-work tags.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing a NIP-13 tag such as ["nonce", "123", "20"] now produces a NonceTag that serializes as ["nonce", "123"], dropping the committed difficulty. Preserve the third field so typed-tag round trips do not remove proof-of-work information.
A successful response whose body is not a JSON object made Moshi throw JsonDataException, escaping callers that handle IOException as documented. Co-authored-by: Janice <6752921105b56ab2273ad065a495f94c1b44ee82067c394bb7cccd0ecc41e033@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jeremy Mawson <jmawson@squareup.com>
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce tag parser still drops NIP-13 proof-of-work data.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing ["nonce", "123", "20"] produces a NonceTag that serializes as ["nonce", "123"], losing the committed difficulty during typed-tag round trips. Preserve the third field so proof-of-work information survives.
rgrnwd
left a comment
There was a problem hiding this comment.
Malformed acknowledgement handling is fixed, but copying event fields can still leave stale signed tags.
| val payload: ByteString? = null, | ||
| val nonce: String? = null, | ||
| override val tags: List<Tag> = listOfNotNull( | ||
| UrlTag(url), |
There was a problem hiding this comment.
The generated copy function preserves existing tags, so copy(url = anotherUrl) or copy(payload = anotherHash) still signs the original values, causing authorization to fail for the new request; GroupChatMessage.copy(groupId = ...) likewise targets the original group. Derive required tags from the current fields and add tests covering these copies.
| .build() | ||
| val acknowledgement = httpClient.newCall(request).execute().use { response -> | ||
| if (!response.isSuccessful) throw IOException("Buzz relay returned HTTP ${response.code}.") | ||
| try { |
There was a problem hiding this comment.
nit: can this be refactored to using result {}?
Co-authored-by: Janice <6752921105b56ab2273ad065a495f94c1b44ee82067c394bb7cccd0ecc41e033@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jeremy Mawson <jmawson@squareup.com>
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still loses proof-of-work data during round trips.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing ["nonce", "123", "20"] creates a NonceTag that serializes as ["nonce", "123"], dropping the NIP-13 committed difficulty. Preserve the third field so typed-tag round trips retain proof-of-work information.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can still leave signed tags inconsistent with their fields.
| val payload: ByteString? = null, | ||
| val nonce: String? = null, | ||
| override val tags: List<Tag> = listOfNotNull( | ||
| UrlTag(url), |
There was a problem hiding this comment.
The generated copy function preserves existing tags, so copy(url = anotherUrl) or copy(payload = anotherHash) signs the original values and fails authorization for the new request; GroupChatMessage.copy(groupId = ...) similarly targets the original group. Derive required tags from the current fields and add tests covering these copies.
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still drops proof-of-work data during round trips.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing ["nonce", "123", "20"] creates a NonceTag that serializes as ["nonce", "123"], losing the NIP-13 committed difficulty. Preserve the third field so typed-tag round trips retain proof-of-work information.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can still leave signed tags inconsistent with their fields.
| val method: String, | ||
| val payload: ByteString? = null, | ||
| val nonce: String? = null, | ||
| override val tags: List<Tag> = listOfNotNull( |
There was a problem hiding this comment.
The generated copy function preserves existing tags, so copy(url = anotherUrl) or copy(payload = anotherHash) signs the original values and fails authorization for the new request; GroupChatMessage.copy(groupId = ...) similarly targets the original group. Derive required tags from the current fields and add tests covering these copies.
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still loses proof-of-work data during typed-tag round trips.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing ["nonce", "123", "20"] creates a NonceTag that serializes as ["nonce", "123"], dropping the NIP-13 committed difficulty. Preserve the third field so round trips retain proof-of-work information.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can leave signed tags inconsistent with their fields.
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still drops proof-of-work data during typed-tag round trips.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing ["nonce", "123", "20"] creates a NonceTag that serializes as ["nonce", "123"], losing the NIP-13 committed difficulty. Preserve the third field so round trips retain proof-of-work information.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can still leave signed tags inconsistent with their fields.
| val method: String, | ||
| val payload: ByteString? = null, | ||
| val nonce: String? = null, | ||
| override val tags: List<Tag> = listOfNotNull( |
There was a problem hiding this comment.
Generated copy functions retain existing tags, so HttpAuth.copy(url = ...) or copy(payload = ...) authorizes the original request, while GroupChatMessage.copy(groupId = ...) still targets the original group. Derive required tags from the current fields and add tests covering these copies.
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still loses proof-of-work data during typed-tag round trips.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can still produce signed tags that disagree with their fields.
| val method: String, | ||
| val payload: ByteString? = null, | ||
| val nonce: String? = null, | ||
| override val tags: List<Tag> = listOfNotNull( |
There was a problem hiding this comment.
Generated copy functions retain existing tags, so HttpAuth.copy(url = ...) or copy(payload = ...) still authorizes the original request, while GroupChatMessage.copy(groupId = ...) still targets the original group. Derive required tags from the current fields and add tests covering these copies.
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still drops proof-of-work data during tag round trips.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can still produce signed tags that disagree with their fields.
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still loses proof-of-work data during tag round trips.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing ["nonce", "123", "20"] creates a NonceTag that serializes as ["nonce", "123"], dropping the NIP-13 committed difficulty. Preserve the third field so round trips retain the information needed to validate proof of work.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can still produce signed tags that disagree with their fields.
jacksoncook
left a comment
There was a problem hiding this comment.
The nonce parser still drops proof-of-work data during tag round trips.
| "u" -> UrlTag(value) | ||
| "method" -> MethodTag(value) | ||
| "payload" -> PayloadTag(value.decodeHex()) | ||
| "nonce" -> NonceTag(value) |
There was a problem hiding this comment.
Parsing ["nonce", "123", "20"] creates a NonceTag that serializes as ["nonce", "123"], losing the NIP-13 committed difficulty needed to validate proof of work. Preserve the third field during round trips.
rgrnwd
left a comment
There was a problem hiding this comment.
Copying the new event models can still produce signed tags that disagree with their fields.
ELI5: Adds a small client that lets any Kotlin service post a chat message to a Buzz channel, so services can reuse one implementation instead of each writing their own.
Adds a dedicated
app.cash.nostrino.buzzpackage, plus the NIP-29 and NIP-98 event types it needs.BuzzClient.postsends a kind-9GroupChatMessageto the community's/eventsendpoint with a NIP-98HttpAuthheader, and succeeds only when the relay acknowledges that exact event id.EventContent.sign(sec, at)signs at a given instant, so a retry with the same inputs re-sends the same event id (the relay stores it once) instead of posting a duplicate.sign(sec)keeps its behaviour.mockwebserver3as a test-only dependency for the client tests.