Repository navigation
fix: adopt paykit 0.1.1-rc6 - #1803
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
[Medium impact] The reviewed changes appear safe to merge; no actionable defect was established. SummaryUpdates Paykit from
No actionable issues were established. Tests and device journeys were not run during this review. Author
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Payment request] --> B[Measure largest payment proof]
B --> C{Fits one message?}
C -- No --> D[Stop before payment]
C -- Yes --> E[Accept and pay]
E --> F[Save and queue proof]
F --> G[Payee verifies USDT transfer]
G --> H{Transfer more than 24 hours before proposal?}
H -- Yes --> I[Show received before request; keep unpaid]
H -- No --> J[Apply amount and deadline checks]
Reviews (1) · Last reviewed commit: "fix: treat unmeasurable paykit proofs as..." · Reviewed by Greptile |
|
Triage: in 3.2.0. This PR closes #1842, filed in the 3.2.0 milestone at 👀 Review: besides the Paykit bump it changes payment and authorization behaviour that ships in 3.2.0, and the profile work builds on it. Twin: synonymdev/bitkit-ios#1237. |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Bumps Paykit to 0.1.1-rc6, measures every Payment Proof with the SDK's paymentProofEventLen before paying (USDT, Lightning, on-chain, and automatic Allowance payments before acceptance), maps message_too_large and signup_code_rejected to their own copy, and flags USDT transfers more than 24h before the request as not paying it. Reviewed 7cc585b, full tier, statically (Bitkit Android branches are not built here).
What I checked, and 5 candidates I ruled out
Read in full (changed regions): PaykitPaymentProofRepo.kt (prepare, submit, proofJson, longestProofJson, requirePaymentProofFits), PaykitAllowanceExecutor.kt (automatic acceptance through payLightning), PaykitUsdtPaymentRepo.kt, PaykitPaymentRequestRepo.kt, PaykitSdkService.paymentProofEventLength, PubkyAuthErrorMessage.kt, PaykitExceptionExt.kt, the AppViewModel send-flow hunk.
Cross-checked against paykit-rs v0.1.1-rc6: payment_proof_event_len serializes through the same serialize_payment_request_event the queue uses, with a fresh v4 event id and request id, so a measurement is the real wire length; the FFI maps is_message_too_large to protocol code message_too_large; proposal_recorded_at is the payee's own receive or record time.
Call sites traced: prepare (manual Lightning and on-chain via AppViewModel.preparePaymentProof, Allowance via prepareProof), associateLightningPayment.
CI: build-local, lint, detekt, Greptile green; build-and-test, ui-tests and the e2e shards still pending at the time of this review.
Ruled out
- Measured proof shorter than the one sent:
longestProofJson()goes through the sameproofJson()helpersubmituses for Bitcoin kinds (64 hex chars, the fixed length of a preimage or txid), and USDT uses the max uint256 log index and full-length hash and signature, so the measurement is an upper bound. - Allowance measures different bindings than it later submits: the pre-acceptance check passes
payment.endpoint.methodId.rawValue,payment.appIdandcandidate.allowanceId, the same valuesprepareProofandassociateLightningPaymentreceive. - Old USDT backups failing to decode:
beforeRequestdefaults tofalsein bothPaykitUsdtReceiptand the backup model;PaykitPaymentStateBackupTest.ktcovers it. - An unmeasurable proof paying anyway: any FFI failure in
paymentProofEventLengthbecomesRequestUnavailablebefore payment, and the Allowance path turns it into MANUAL before acceptance. - Transport mapping hiding a more specific auth error:
localizedPubkyAuthMessagewalks from the outermost throwable, so the specificPubkyAuthRequestErrorcases aboveTransportstill win on a wrapped error.
Merge confidence: 4/5, payment and proof paths with unit coverage but build-and-test and the e2e shards had not finished, and J1, J2 and Manual 1 to 3 need a device.
Reviewer model: claude-opus-5-5
Reasoning effort: Not exposed (the session does not report a reasoning setting)
There was a problem hiding this comment.
Verdict: ✅ Approve
Findings: None.
QA for 54b6ee4: Android 16, dev/regtest build with Paykit 0.1.1-rc6. All five cases passed. Videos run at 2× speed.
Shorten an oversized payment request note ✅ The long note was refused; shortening it to Coffee delivered the 1,000-sat request with the correct confirmation.
I pasted 256 purple hearts, observed “Shorten the note and try again.”, and checked that the payer had no new request. Coffee then sent successfully. I repeated delivery with a funded payer, checked the amount and note, and closed confirmation without paying.
| Note rejection and delivery | Funded payer confirmation |
|---|---|
Requester on the left; payer on the right.
Reject an invalid signup code ✅ A refused signup token showed the specific homeserver message, kept authorization available, and created no identity.
I authorized a signup-grant link containing an invalid token against the staging homeserver. The recording shows the specific error toast. The app log reports Identity / signup_code_rejected; the wallet retained Profile onboarding.
Refused signup-code authorization.
Refuse a Bitcoin request whose proof cannot fit ✅ Attempting payment showed “Payment request is unavailable” and preserved the payer’s full balance.
A linked issuer with a debug-only hook sent a valid one-time proposal with a 594-byte reference. Paykit’s native measurement reported a 1,001-byte on-chain proof. On the unchanged payer, swiping to pay returned Home with the error and the full 100,000-sat balance; its log reports RequestUnavailable from proof preparation.
Payment refused before any Bitcoin was sent.
test hook
Not pushed. The issuer hook supplies a custom reference, removes optional proposal fields, selects one Bitcoin endpoint, and logs Paykit’s actual proof measurement. The receiving app used the unchanged reviewed head.
diff --git a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt
index 877c5a043..a7e65dadd 100644
--- a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt
@@ -955,11 +955,15 @@ class PaykitPaymentRequestRepo @Inject constructor(
amountValue = draft.amount.value,
amountAsset = draft.amount.asset.code,
conversion = rates.takeIf { it.isNotEmpty() }?.let(PaymentConversion::Fixed),
- paymentDeadline = PaymentDeadline.At(draft.expiresAt.toString()),
- paymentReference = "bitkit-${UUID.randomUUID()}",
+ paymentDeadline = if (to.bitkit.BuildConfig.DEBUG && java.io.File(Env.logDir.parentFile, "v4-request-reference").exists()) null else PaymentDeadline.At(draft.expiresAt.toString()), // test hook
+ paymentReference = "bitkit-${UUID.randomUUID()}".let { original -> // test hook
+ if (to.bitkit.BuildConfig.DEBUG) { // test hook
+ java.io.File(Env.logDir.parentFile, "v4-request-reference").takeIf { it.exists() }?.readText() ?: original // test hook
+ } else original // test hook
+ }, // test hook
proposalExpiresAt = draft.expiresAt.toString(),
- acceptedPaymentEndpointIdentifiers = endpointIdentifiers,
- metadataJson = JsonObject(mapOf("note" to JsonPrimitive(draft.note))).toString(),
+ acceptedPaymentEndpointIdentifiers = if (to.bitkit.BuildConfig.DEBUG && java.io.File(Env.logDir.parentFile, "v4-request-reference").exists()) listOf("btc-regtest-p2wpkh") else endpointIdentifiers, // test hook
+ metadataJson = if (to.bitkit.BuildConfig.DEBUG && java.io.File(Env.logDir.parentFile, "v4-request-reference").exists()) "{}" else JsonObject(mapOf("note" to JsonPrimitive(draft.note))).toString(), // test hook
)
}
diff --git a/app/src/main/java/to/bitkit/services/PaykitSdkService.kt b/app/src/main/java/to/bitkit/services/PaykitSdkService.kt
index 1301df5fb..36ddb8bd5 100644
--- a/app/src/main/java/to/bitkit/services/PaykitSdkService.kt
+++ b/app/src/main/java/to/bitkit/services/PaykitSdkService.kt
@@ -1103,7 +1103,35 @@ class PaykitSdkService @Inject constructor(
}
}
- suspend fun paymentRequests(): List<PaymentRequestRecord> = allPaymentRequests().filter(::isBitkitPaymentRequest)
+ suspend fun paymentRequests(): List<PaymentRequestRecord> { // test hook
+ val records = allPaymentRequests().filter(::isBitkitPaymentRequest) // test hook
+ if (to.bitkit.BuildConfig.DEBUG) { // test hook
+ val dump = org.json.JSONArray() // test hook
+ records.forEach { r -> // test hook
+ dump.put(org.json.JSONObject().put("id", r.paymentRequestId).put("peer", r.counterparty) // test hook
+ .put("state", r.state.toString()).put("role", r.localRole.toString()) // test hook
+ .put("reference", r.terms?.paymentReference?.exportText()).put("proposalRecordedAt", r.proposalRecordedAt) // test hook
+ .put("terms", r.terms.toString()).put("proofCount", r.paymentProofs.size)) // test hook
+ } // test hook
+ java.io.File(context.filesDir, "v4-requests.json").writeText(dump.toString()) // test hook
+ val command = java.io.File(context.filesDir, "v4-proof-command.json") // test hook
+ val busy = java.io.File(context.filesDir, "v4-proof-command.busy") // test hook
+ if (command.exists() && command.renameTo(busy)) { // test hook
+ runSuspendCatching { // test hook
+ val input = org.json.JSONObject(busy.readText()) // test hook
+ val r = records.single { it.paymentRequestId == input.getString("id") } // test hook
+ if (r.state == com.synonym.paykit.PaymentRequestLifecycleState.PROPOSED) { // test hook
+ acceptPaymentRequest(r.counterparty, r.paymentRequestId, requireNotNull(r.proposalStreamItemId)) // test hook
+ } // test hook
+ submitPaymentProof(r.counterparty, r.paymentRequestId, "bitkit", "usdt-arbitrum-address", input.getJSONObject("proof").toString()) // test hook
+ processPendingPrivateMessages() // test hook
+ busy.renameTo(java.io.File(context.filesDir, "v4-proof-command.done")) // test hook
+ Logger.info("Submitted controlled old-transfer proof for '${r.paymentRequestId}'", context = "TestHook") // test hook
+ }.onFailure { Logger.warn("Failed controlled proof", it, context = "TestHook") } // test hook
+ } // test hook
+ } // test hook
+ return records // test hook
+ } // test hook
suspend fun readPaymentRequests(counterparty: String, expectedIdentity: String? = null): PaymentRequestRead {
isSetup.await()
@@ -1220,7 +1248,7 @@ class PaykitSdkService @Inject constructor(
val terms = PaymentRequestTerms(
amount = PaymentRequestAmount(proposal.amountValue, proposal.amountAsset),
paymentReference = PaymentReference(proposal.paymentReference),
- proposalExpiresAt = proposal.proposalExpiresAt,
+ proposalExpiresAt = if (to.bitkit.BuildConfig.DEBUG && java.io.File(context.filesDir, "v4-request-reference").exists()) null else proposal.proposalExpiresAt, // test hook
recurrence = proposal.recurrence?.let {
PaymentRequestRecurrence(
every = it.every,
@@ -1237,6 +1265,9 @@ class PaykitSdkService @Inject constructor(
paymentDeadline = proposal.paymentDeadline,
metadata = PrivateJsonObject(proposal.metadataJson),
)
+ if (to.bitkit.BuildConfig.DEBUG) { // test hook
+ Logger.info("Fixture proposal refBytes='${proposal.paymentReference.toByteArray().size}' amount='${proposal.amountValue}' endpoints='${proposal.acceptedPaymentEndpointIdentifiers}' deadline='${terms.paymentDeadline}' expiry='${terms.proposalExpiresAt}' metadata='${terms.metadata.exportText()}' proofBytes='${paymentProofEventLength(proposal.paymentReference, "bitkit", "btc-regtest-p2wpkh", "{\"data\":\"${"0".repeat(64)}\",\"type\":\"bitcoin-onchain-txid\"}")}'", context = "TestHook") // test hook
+ } // test hook
completeSdkCall { handle.proposePaymentRequest(counterparty, terms) }
}
}Fixture proposal refBytes='594' amount='0.00001' endpoints='[btc-regtest-p2wpkh]' deadline='null' expiry='null' metadata='{}' proofBytes='1001'
Failed to prepare a Paykit payment proof [RequestUnavailable='Payment request is unavailable']
Keep an old USDT transfer from paying a new request ✅ The list and detail showed “Received · Before request · 1.00 USDT” rather than Paid.
I sent a genuine 1 USDT transfer on the local fixture chain, dated more than 48 hours before the new $1 request. After restoring the fixture clock, the payer signed a proof bound to that request and submitted it through Paykit using a debug-only hook. The unchanged payee verified it and showed the “Before request” status in both views. The funds remained received.
| Payment Requests | Request detail |
|---|---|
Old transfer shown without a Paid status.
test hook
Not pushed. The sender hook exports request bindings and submits a supplied signed proof through the real Paykit accept and submit operations. It does not alter proof verification or payment-time decisions. The receiving app used the unchanged reviewed head.
diff --git a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt
index 877c5a043..a7e65dadd 100644
--- a/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt
@@ -955,11 +955,15 @@ class PaykitPaymentRequestRepo @Inject constructor(
amountValue = draft.amount.value,
amountAsset = draft.amount.asset.code,
conversion = rates.takeIf { it.isNotEmpty() }?.let(PaymentConversion::Fixed),
- paymentDeadline = PaymentDeadline.At(draft.expiresAt.toString()),
- paymentReference = "bitkit-${UUID.randomUUID()}",
+ paymentDeadline = if (to.bitkit.BuildConfig.DEBUG && java.io.File(Env.logDir.parentFile, "v4-request-reference").exists()) null else PaymentDeadline.At(draft.expiresAt.toString()), // test hook
+ paymentReference = "bitkit-${UUID.randomUUID()}".let { original -> // test hook
+ if (to.bitkit.BuildConfig.DEBUG) { // test hook
+ java.io.File(Env.logDir.parentFile, "v4-request-reference").takeIf { it.exists() }?.readText() ?: original // test hook
+ } else original // test hook
+ }, // test hook
proposalExpiresAt = draft.expiresAt.toString(),
- acceptedPaymentEndpointIdentifiers = endpointIdentifiers,
- metadataJson = JsonObject(mapOf("note" to JsonPrimitive(draft.note))).toString(),
+ acceptedPaymentEndpointIdentifiers = if (to.bitkit.BuildConfig.DEBUG && java.io.File(Env.logDir.parentFile, "v4-request-reference").exists()) listOf("btc-regtest-p2wpkh") else endpointIdentifiers, // test hook
+ metadataJson = if (to.bitkit.BuildConfig.DEBUG && java.io.File(Env.logDir.parentFile, "v4-request-reference").exists()) "{}" else JsonObject(mapOf("note" to JsonPrimitive(draft.note))).toString(), // test hook
)
}
diff --git a/app/src/main/java/to/bitkit/services/PaykitSdkService.kt b/app/src/main/java/to/bitkit/services/PaykitSdkService.kt
index 1301df5fb..36ddb8bd5 100644
--- a/app/src/main/java/to/bitkit/services/PaykitSdkService.kt
+++ b/app/src/main/java/to/bitkit/services/PaykitSdkService.kt
@@ -1103,7 +1103,35 @@ class PaykitSdkService @Inject constructor(
}
}
- suspend fun paymentRequests(): List<PaymentRequestRecord> = allPaymentRequests().filter(::isBitkitPaymentRequest)
+ suspend fun paymentRequests(): List<PaymentRequestRecord> { // test hook
+ val records = allPaymentRequests().filter(::isBitkitPaymentRequest) // test hook
+ if (to.bitkit.BuildConfig.DEBUG) { // test hook
+ val dump = org.json.JSONArray() // test hook
+ records.forEach { r -> // test hook
+ dump.put(org.json.JSONObject().put("id", r.paymentRequestId).put("peer", r.counterparty) // test hook
+ .put("state", r.state.toString()).put("role", r.localRole.toString()) // test hook
+ .put("reference", r.terms?.paymentReference?.exportText()).put("proposalRecordedAt", r.proposalRecordedAt) // test hook
+ .put("terms", r.terms.toString()).put("proofCount", r.paymentProofs.size)) // test hook
+ } // test hook
+ java.io.File(context.filesDir, "v4-requests.json").writeText(dump.toString()) // test hook
+ val command = java.io.File(context.filesDir, "v4-proof-command.json") // test hook
+ val busy = java.io.File(context.filesDir, "v4-proof-command.busy") // test hook
+ if (command.exists() && command.renameTo(busy)) { // test hook
+ runSuspendCatching { // test hook
+ val input = org.json.JSONObject(busy.readText()) // test hook
+ val r = records.single { it.paymentRequestId == input.getString("id") } // test hook
+ if (r.state == com.synonym.paykit.PaymentRequestLifecycleState.PROPOSED) { // test hook
+ acceptPaymentRequest(r.counterparty, r.paymentRequestId, requireNotNull(r.proposalStreamItemId)) // test hook
+ } // test hook
+ submitPaymentProof(r.counterparty, r.paymentRequestId, "bitkit", "usdt-arbitrum-address", input.getJSONObject("proof").toString()) // test hook
+ processPendingPrivateMessages() // test hook
+ busy.renameTo(java.io.File(context.filesDir, "v4-proof-command.done")) // test hook
+ Logger.info("Submitted controlled old-transfer proof for '${r.paymentRequestId}'", context = "TestHook") // test hook
+ }.onFailure { Logger.warn("Failed controlled proof", it, context = "TestHook") } // test hook
+ } // test hook
+ } // test hook
+ return records // test hook
+ } // test hook
suspend fun readPaymentRequests(counterparty: String, expectedIdentity: String? = null): PaymentRequestRead {
isSetup.await()
@@ -1220,7 +1248,7 @@ class PaykitSdkService @Inject constructor(
val terms = PaymentRequestTerms(
amount = PaymentRequestAmount(proposal.amountValue, proposal.amountAsset),
paymentReference = PaymentReference(proposal.paymentReference),
- proposalExpiresAt = proposal.proposalExpiresAt,
+ proposalExpiresAt = if (to.bitkit.BuildConfig.DEBUG && java.io.File(context.filesDir, "v4-request-reference").exists()) null else proposal.proposalExpiresAt, // test hook
recurrence = proposal.recurrence?.let {
PaymentRequestRecurrence(
every = it.every,
@@ -1237,6 +1265,9 @@ class PaykitSdkService @Inject constructor(
paymentDeadline = proposal.paymentDeadline,
metadata = PrivateJsonObject(proposal.metadataJson),
)
+ if (to.bitkit.BuildConfig.DEBUG) { // test hook
+ Logger.info("Fixture proposal refBytes='${proposal.paymentReference.toByteArray().size}' amount='${proposal.amountValue}' endpoints='${proposal.acceptedPaymentEndpointIdentifiers}' deadline='${terms.paymentDeadline}' expiry='${terms.proposalExpiresAt}' metadata='${terms.metadata.exportText()}' proofBytes='${paymentProofEventLength(proposal.paymentReference, "bitkit", "btc-regtest-p2wpkh", "{\"data\":\"${"0".repeat(64)}\",\"type\":\"bitcoin-onchain-txid\"}")}'", context = "TestHook") // test hook
+ } // test hook
completeSdkCall { handle.proposePaymentRequest(counterparty, terms) }
}
}Transfer receipt timestamp: 2026-11-08T15:41:20Z
New proposal recorded by payer: 2026-11-10T15:59:30.281121955+00:00
Submitted controlled old-transfer proof for '8279aaf6-34af-4cd0-9313-378154c98c8c'
PaymentRequestDetailsStatus: Received · Before request · 1.00 USDT
Explain an offline signup authorization ✅ Authorize showed “Check your connection and try again.” and kept the sheet open.
I disabled Wi-Fi and mobile data before authorizing the signup link. The app log reports Transport / transport_error during homeserver resolution. Networking was restored afterward.
Offline authorization with the connection message and enabled controls.
Check: build and changed unit suites ✅ The head built and the selected repository, backup, authentication and send-flow suites passed.
I built the dev/debug app and ran the nine changed suite selectors: 1,235 tests, no failures, errors or skips. The resolved Paykit AAR matched the SHA-256 pinned in this PR.
I reviewed the full 1764309..54b6ee4 diff and its payment callers. Automatic Allowance preflight, Lightning proof sizing, backup compatibility and subscription error mapping were covered by code review and unit tests, without separate device cases. No planned device case was left unrun.
The Android behavior and journey actions match synonymdev/bitkit-ios#1237 fix: adopt paykit 0.1.1-rc6; I did not run the iOS app.
I also read Talos’s earlier review; its static review was context, and the device results above are from this head.
Models used: review and device validation — gpt-6.1-sol, reasoning high; independent diff review — gpt-6.1-sol, reasoning high.
Reviewed by gpt-6.1-sol-high
jvsena42
left a comment
There was a problem hiding this comment.
Code scan at this head found no HIGH, MEDIUM or LOW issues. The device gate still has to run; I'll post its result in a follow-up review.
What I checked in the rc2 → rc6 SDK changes:
- New error codes:
message_too_largeandidentity/signup_code_rejectedare mapped to the new copy. - Sign-up/sign-in errors now
Transport: these already go to the deferred-restore and temporary-error paths. - Proof size check:
paymentProofEventLenruns before anything is claimed or persisted, for Lightning, on-chain, USDT and allowance requests. The placeholder proof matches the real proof's shape, so the measured size is at least the sent size. - USDT "before request":
proposalRecordedAtcomes from the outbound message'screated_at, which rc2 already stored. Receipts verified before the upgrade don't flip to "Before request".beforeRequestdefaults to false, so backups from rc2 builds still decode.
stateRevisionbusy error: it only fires under callback storage. Bitkit usesPubkySharedand already maps read failures to null.- State migration: the state blob goes from v1 to v4. A single-device upgrade from 3.1.0 reads v1 fine. As the PR body says, an rc2 build can't read state once an rc6 build has written it.
Out of scope: a user with a 3.1.0 device and a 3.2.0 device on the same Pubky identity loses Paykit on the old device until it updates. The PR accepts this ("ship together"). It would help to add a line about it to the release notes.
Reviewer model: claude-fable-5-1 (Stage 1), claude-opus-5-5 (coordination)
Reasoning effort: not exposed (Fable subagent), default (Opus)
|
@ben-kaufman merged, as it was approved and green. |
Closes #1842
Twin: synonymdev/bitkit-ios#1237
This PR:
Description
com.synonym:paykit-androidto0.1.1-rc6, the same one-line change as earlier Paykit bumps. The bump itself only needed test fixtures for the new Payment Request record times and the private list acknowledgement field.paymentProofEventLento measure the largest Payment Proof Bitkit would send, so the check matches what the SDK queues instead of a hand-built JSON copy. A proof that would not fit one Paykit message is refused before anything is paid, because it could never reach the payee. USDT already had this check. Lightning and on-chain Payment Request payments, manual and automatic, now have it too. An automatic Allowance payment measures before it accepts the request, so an oversized one stays manual instead of being accepted and then failing to pay.webpki-roots1.0.11 list. The staging and production homeserver, relay, pkarr and homegate hosts all chain to ISRG Root X1, which that list includes.Out of Scope
PaykitAllowanceRepo: accepting a contact's pending allowance offers withacceptPendingAllowanceProposals. The bulk call also accepts offers Bitkit does not list, such as non-BTC terms, and drops the per-proposal binding, so offers are still accepted one at a time. iOS made the same call.Design
N/A - no design available.
Preview
N/A
QA Notes
Setup
com.synonym:paykit-android:0.1.1-rc6from GitHub Packages (AAR SHA-256792ace22d4e188733df3ec3644d69e22fb6692f39115ce55b706f91edc3ccdd8).request-summary.xml. Put the 256 "💜" note on the requester's clipboard, sinceadb shell input textcannot type emoji.pubkyauth://signup_grantURL whose signup token the homeserver refuses, for example the token of a link another wallet already completed, and a wallet with no Pubky identity. See the pubky-auth suite README.docs/usdt-qa.mdand a payer that submits a Payment Proof for a USDT transfer made more than 24 hours before the request.Journeys
request-note-too-long.xml- a note too long for one Paykit message is refused with "Shorten the note and try again." before anything is sent, and the request sends once the note is shortenedgrant-signup-rejected-code.xml- a refused signup code shows its own message, keeps the sheet open and creates no identityManual Tests
Automated Checks
PaykitPaymentProofRepoTest.kt- a Lightning proof is measured with its largest preimage, app and allowance before payment, an on-chain proof too large for one message is refused before payment, and a proof the SDK cannot measure is refused as unavailablePaykitAllowanceExecutorTest.kt- the proof is measured with the Allowance ID before automatic acceptance, and a request whose proof would not fit stays manual without acceptancePaykitUsdtPaymentRepoTest.kt- the USDT proof is measured by the SDK with the largest valid receipt log index (fits and too large). Transfers within the 24 h tolerance pay the request and older ones are flagged and do notPaykitPaymentStateBackupTest.kt- a USDT backup without the new flag still decodes, and the flag round tripsAppViewModelSendFlowTest.kt- a request too long to send shows "Shorten the note and try again." and does not completePaykitPaymentRequestRepoTest.kt-message_too_largemaps to the note error, and other protocol errors pass through unchangedPaykitPaymentRequestRepoSubscriptionTest.kt- an SDKmessage_too_largemaps to the subscription messagePubkyAuthErrorMessageTest.kt- refused signup codes and transport errors get their own text, and other identity errors keep the fallbackPubkyRepoTest.kt- a sign-up refused because the identity lives elsewhere signs in, and a transport failure does not sign up againPaykitIssuerInteropTest.kt,PrivatePaykitRepoTest.kt,PaykitSdkServiceTest.kt- fixtures for the new rc6 record and report fieldsPaykitUsdtPaymentRepoTest.ktboundary cases for the hand-built proof JSON - the SDK now does the measurementopenssl s_client -showcertsagainst httprelay, homeserver, pkarr and homegate (production and staging),pkarr.pubky.organdrelay.pkarr.org- every chain ends at ISRG Root X1Evidence
7cc585b5:just compile,just testandjust lintpass locally (4,955 unit tests in 264 suites, 0 failures or skips, 0 detekt findings, 0 Android Lint issues).Gaps
<action>steps as the iOS twin (chore: adopt Paykit 0.1.1-rc6 bitkit-ios#1237), apart from platform identifiers.Models used
Claude Opus 5.5(reasoning: Not exposed)Claude Opus 5.5(reasoning: Not exposed)Claude Opus 5.5(reasoning: Not exposed)The session metadata does not expose a reasoning setting.
🤖 Generated with Claude Code