feat(connector): add Payhound crypto connector (Authorize, PSync, Incoming Webhooks) - #2173
Open
shuklatushar226 wants to merge 132 commits into
Open
feat(connector): add Payhound crypto connector (Authorize, PSync, Incoming Webhooks)#2173shuklatushar226 wants to merge 132 commits into
shuklatushar226 wants to merge 132 commits into
Conversation
…hooks)
Add Payhound, a hosted crypto invoice gateway (Malta MFSA/MiCAR licensed),
integrated as a Cryptopay-style hosted redirect.
Scope: Crypto payment method only (PaymentMethodData::Crypto). Payhound's API
has no card or wallet concept.
Flows:
- Authorize: POST /api/v1/invoices, returns a hosted redirect
- PSync: GET /api/v1/invoices/{id}
- Incoming webhooks: HMAC-SHA512 source verification, status and
captured-amount extraction
Not implemented (Payhound documents no such endpoints; declared not_supported
rather than stubbed): capture, void, refund, rsync, mandates, disputes, 3DS.
MANUAL capture is rejected up front with CaptureMethodNotSupported.
Notable details:
- Amounts are decimal major-unit strings ("266.45"), via StringMajorUnit
- Auth is HMAC-SHA512 over uri_path ++ nonce ++ hex(SHA256(body)), sent as
X-MB-Key / X-MB-Nonce / X-MB-Signature with Content-Type application/vnd.api+json
- Nonce is a process-global AtomicU64, max(now_micros, prev+1) under SeqCst CAS
- invoice_url may come back relative or absolute; both are handled
Proto/core: adds Connector::PAYHOUND = 140, message PayhoundConfig, the
ConnectorSpecificConfig oneof field, ConnectorEnum::Payhound and the
ConnectorAuthType -> config conversion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3mj9nTAduEyXEKMs3Ms8W
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
… stable The payhound probe artifact changed on every regeneration, so CI's auto-fix job committed it, which re-triggered CI, which regenerated it again — 125 identical "chore: auto-fix formatting and generated code" commits on this branch in ~17 hours, netting +15/-4. Payhound's X-MB-Nonce is a microsecond-resolution epoch, so it is 16 digits wide. normalize_content cannot catch it: replace_timestamps rewrites runs of exactly 13 or 14 digits and deliberately leaves 16-digit runs alone so card numbers are never touched. Pin it by header name in normalize_header_value instead, alongside the existing salt/idempotency-key/timestamp entries, which leaves the card-number guard intact. Also corrects the replace_timestamps doc comment, which claimed "13+ consecutive digits" while the code is exactly-13-or-14 — the mismatch is what made the nonce look like it should already have been covered. The regression test asserts both halves: two different nonces normalize equal, and a bare 16-digit run in a body is still copied verbatim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D3mj9nTAduEyXEKMs3Ms8W
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Payhound as a new UCS connector. Payhound is a hosted crypto invoice gateway (Malta,
MFSA / MiCAR licensed), integrated in the Cryptopay style: UCS creates an invoice, Payhound returns a
hosted payment page, the buyer pays on-chain there, and settlement is reported back by callback and
by polling.
Generated and validated by GRACE (automated connector integration pipeline).
1. Scope
PaymentMethodData::Crypto/CryptoData. No cards, nowallets. Payhound's API has no such concept, so any non-crypto payment method is rejected in the
transformer (unit-tested:
non_crypto_payment_method_is_rejected).2. Flows implemented
POST /api/v1/invoicesGET /api/v1/invoices/{id}3. Flows deliberately NOT implemented — and why
The Payhound API documents no refund, void, capture or refund-sync endpoints anywhere across its
25 documentation pages (see
data/integration-source-links.jsonfor the full doc index committedin this PR). These are therefore declared
not_supportedrather than stubbed with something thatwould silently misbehave:
MANUALcapture is rejected up frontwith
CaptureMethodNotSupportedrather than emitting an authorization that could never besettled.
aborted.refund out-of-band from their Payhound balance.
4. Cross-repo note
This is the UCS half of a two-repo change. The Hyperswitch-router half is a separate PR against
juspay/hyperswitch, which adds the connector enum variant, theucs_only_connectorsroutingentry, a stub connector and the config. The two must land together for the end-to-end path to
work; this PR alone gives you the UCS-side gRPC surface only.
5. Proto and core domain_types changes (declared explicitly)
This PR necessarily touches
proto/and coredomain_types. Stating it plainly rather than buryingit in the diff:
crates/types-traits/grpc-api-types/proto/payment.protoPAYHOUND = 140;added toenum Connector(next free number; verified no collision onmain).PayhoundConfig payhound = 150;added to theConnectorSpecificConfigoneof (next free fieldnumber).
domain_typesConnectorEnum::PayhoundConnectorSpecificConfig::Payhound { api_key, api_secret, base_url }ConnectorAuthType -> ConnectorSpecificConfigconversion. It acceptsSignatureKey(
key1unused — Payhound has no third credential) andBodyKey.All other touched files are the standard additive registration points:
connectors.rs,default_implementations.rs,types.rs,field-probe/src/auth.rs,sdk/rust/smoke-test, and thefour
config/*.tomlfiles. Every hunk in this PR is additive; there is no unrelated churn.6. Notable implementation details for reviewers
Amounts are decimal major-unit strings.
priceis"266.45", not minor units.StringMajorUnitconverter. Verified live:minor_amount: 26645->"266.45".Auth is HMAC-SHA512 with
X-MB-Key/X-MB-Nonce/X-MB-SignatureandContent-Type: application/vnd.api+json:The three worked signing vectors published in Payhound's own authentication docs are committed as
unit tests (
sign_post_vector,sign_get_query_vector,callback_signature_vector). Thoseconstants are documentation example values, not live credentials.
The nonce must be unique AND strictly increasing forever, per API key. Implemented as a
process-global
AtomicU64computingmax(now_micros, prev + 1)under aSeqCstCAS. Two unittests cover it: 10,000 serial calls strictly increasing, and 8 threads x 2,000 calls all distinct.
Known bound worth flagging: the counter is process-global, not cluster-global. Separate
replicas sharing one API key each keep their own counter. This is worth reviewing if a
multi-replica deployment is planned.
invoice_urlcomes back RELATIVE from the sandbox (/invoices/{id}) but ABSOLUTE in thedocs. The redirect builder handles both (
invoice_url_relative_is_resolved,invoice_url_absolute_passes_through). The signeduri_pathis derived from the parsed requestURL, and
base_urlistrim_end_matches('/'), so a trailing slash in config cannot corrupt thesignature.
Status mapping, with the two judgement calls justified in code comments:
pendingAuthenticationPendingcompletedChargedaborted,timeoutFailureoverpaidChargedUnresolvedwould leave a fully-paid order permanently unfulfilled.underpaidPendingcompleted/overpaid/timeout.Failurewould prematurely kill an order the buyer can still top up.#[serde(other)] UnknownPending+tracing::warn!Files
Added
crates/integrations/connector-integration/src/connectors/payhound.rscrates/integrations/connector-integration/src/connectors/payhound/transformers.rscrates/internal/integration-tests/src/connector_specs/payhound/specs.jsondata/field_probe/payhound.jsondocs-generated/connectors/payhound.mdexamples/payhound/{payhound.rs,payhound.py,payhound.ts,payhound.kt}Modified (all additive)
config/development.toml,config/production.toml,config/sandbox.toml,config/superposition.tomlcrates/integrations/connector-integration/src/connectors.rscrates/integrations/connector-integration/src/default_implementations.rscrates/integrations/connector-integration/src/types.rscrates/internal/field-probe/src/auth.rscrates/types-traits/domain_types/src/connector_types.rscrates/types-traits/domain_types/src/router_data.rscrates/types-traits/domain_types/src/types.rscrates/types-traits/grpc-api-types/proto/payment.protodata/integration-source-links.json(carries the Payhound doc URLs)sdk/rust/smoke-test/src/build_auth.rsBase URLs:
production.toml->https://api.payhound.com(live host);sandbox.toml/development.toml->https://sandbox-api.payhound.com.gRPC test results
Status: PASS (Authorize
201, PSync200)grpcurl transcript against the live Payhound sandbox (credentials never inlined)
Server:
cargo run --bin grpc-serveronlocalhost:8000.The
x-connector-configheader is built fromcreds.jsonwithjqso the transcript isreproducible without pasting any credential:
1. Authorize — Crypto (BTC), EUR 266.45
Body UCS actually signed and sent to
POST https://sandbox-api.payhound.com/api/v1/invoices:{"currency":"EUR","price":"266.45","invoice_currency":"BTC", "description":"Payhound UCS smoke test invoice","reference":"payhound_pr_authorize_001", "callback_url":"...","success_url":"...","cancel_url":"...","notify_email":"*****@example.com"}Note
priceis the decimal major-unit string"266.45"derived fromminor_amount: 26645, andabsent optional fields are omitted rather than serialized as
null.Response:
{ "merchantTransactionId": "TSTINCUSTOMREF", "connectorTransactionId": "3424d1dc689a3b42bb00b0b1719588b5", "status": "AUTHENTICATION_PENDING", "statusCode": 201, "redirectionData": { "form": { "endpoint": "https://pay.payhound.com/invoices/3424d1dc689a3b42bb00b0b1719588b5", "method": "HTTP_METHOD_GET" } }, "connectorReferenceId": "TSTINCUSTOMREF" }The sandbox returned the relative
invoice_url/invoices/3424d1dc...; UCS resolved it to theabsolute hosted-invoice URL shown above. The crypto
addressisSecret-wrapped and masks as*** alloc::string::String ***in the typed connector response.2. PSync —
GET /api/v1/invoices/{id}Response:
statusCode 200,status AUTHENTICATION_PENDING, amount26645 EUR, redirect endpointrebuilt correctly.
Not proven
These are the honest limits of the evidence above. Please read them as written.
every response echoes name
"Test Invoice", reference"TSTINCUSTOMREF", merchant_amount"20.00"and a freshly invented id, whatever id is requested. UCS signed and sent the correctURL and got a
200describing a different invoice. This proves signing, headers, content type,URL construction, deserialization, status mapping and relative-URL resolution — not id-specific
retrieval.
completed/overpaid/underpaid/aborted/timeout) aredoc-mapped and fixture-tested but never observed live; the sandbox only ever returns
pending.rests on a unit vector built from the documented signing string, not a captured live callback.
tests.
Validation checklist
cargo buildfull workspace clean, zero errorscargo clippy— zero payhound hitscargo fmtcleancargo test -p connector-integration payhound— 19 passed, 0 failed201,AUTHENTICATION_PENDING)200creds.jsonvalues — no match)config/production.tomlpoints at the live hosthttps://api.payhound.com