Repository navigation
chore: adopt Paykit 0.1.1-rc6 - #1237
Conversation
Pin paykit-rs to the published 0.1.1-rc6 release (revision 9919198a) and update test fixtures for the new Payment Request record times and the acknowledged list in Private Payment List delivery reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measure the Payment Proof Event Message with the SDK's paymentProofEventLen before a Lightning, on-chain or USDT Payment Request payment, so a request whose proof cannot fit one Paykit message is refused before anything is paid. The USDT check now uses the same measurement instead of a hand-built copy of the message. Ask to shorten the note or subscription text when the SDK refuses a proposal with message_too_large, and explain a refused signup code or a failed connection in the Pubky authorization sheet. A verified USDT transfer made more than a day before this wallet recorded its request is labelled Received, Before request and no longer counts as paying it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measure the Payment Proof Event Message before an Allowance accepts a Payment Request automatically, so a request whose proof cannot fit one Paykit message is left for manual review instead of being accepted and then failing to pay. Allowance test fixtures now use canonical UUID-v4 Allowance IDs, which the SDK requires in a Payment Proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover that the proof size check counts the Allowance ID, Billing Period and quote ID it will send, that Bitkit's own requests leave room for their longest proof, that SDK-formatted record times are parsed, that Before request wins over the other receipt labels, and that receipts saved before the flag existed still count as paid. Use the SDK App ID and the USDT chain ID from their shared constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[High impact] The reviewed changes appear safe to merge, with no actionable issues found. SummaryAdopts Paykit
Scope acknowledged by
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Payment request] --> B[Measure longest payment proof]
B --> C{Fits one message?}
C -- No --> D[Show request unavailable]
C -- Yes --> E[Prepare payment]
E --> F[Pay and send proof]
G[Verified USDT transfer] --> H{Too far before request?}
H -- Yes --> I[Show Before request and leave unpaid]
H -- No --> J[Check amount and deadline]
Reviews (1) · Last reviewed commit: "chore: rename changelog fragment" · Reviewed by Greptile |
|
Triage: in 3.2.0. This PR closes #1271, 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-android#1803. |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. iOS twin of synonymdev/bitkit-android#1803: pins Paykit 0.1.1-rc6 (project and Package.resolved agree on the tag), measures every Payment Proof with Paykit.paymentProofEventLen before paying (USDT, Lightning, on-chain, and automatic Allowance payments before the execution claim), maps message_too_large and signup_code_rejected to their own copy, and flags USDT transfers more than 24h before the request. Reviewed 1594952, full tier, statically: iOS does not build on Linux, so CI is the gate.
What I checked, and 5 candidates I ruled out
Read in full (changed regions): PaykitPaymentProofService.swift (PaykitPaymentProofSize, prepare, proofText), PaykitAllowanceExecutor.swift (the pre-claim check), PaykitUsdtPaymentService.swift, PaykitPaymentRequestService.swift, PaykitUsdtStateBackup.swift, PubkyAuthApprovalSheet.swift, PaymentRequestsView.swift, PubkyService.swift.
Cross-checked against paykit-rs v0.1.1-rc6: payment_proof_event_len serializes through the queue's own serialize_payment_request_event; the FFI raises protocol code message_too_large; proposal_recorded_at is the payee's own record time.
CI: an earlier build-local run failed at "Verify current PR merge checkout" (the base moved under it, before any compile); the rerun, SwiftFormat and validate were pending at the time of this review.
Ruled out
- Placeholder proof shorter than the real one: the Bitcoin placeholder uses the same
{"data","type"}keys asproofTextwith a 64-hex value, the fixed length of a preimage or txid; the USDT one carries the same five keys asProofwith the max uint256 log index. Key order differs fromJSONEncoder's, which changes no length. - Allowance measures a different quote than it pays with: passing a quote id can only lengthen the measured message, so the pre-claim check is never looser than
prepare's own check, which runs again with the boundproof.conversionQuoteId. - Old receipts decoding:
beforeRequestis optional in both the receipt and the backup, restored asfalse, andsatisfiedtreatsnilas not-before;PaykitPaymentStateBackupTests.swiftcovers the missing key. - Restored receipts churning on reconcile: a restored
nilagainst a computedfalsefailscontainsand the receipt is replaced by the same-id fresh one, a one-time rewrite, not a duplicate. - Generic auth errors losing their text:
pubkyAuthApprovalFailureDescriptionfalls through tolocalizedDescriptionfor everything but the two new cases.
Merge confidence: 4/5, payment and proof paths with unit tests but no CI result on this head yet, 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)
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)
There was a problem hiding this comment.
Verdict:
Findings: None.
QA for a1c126d: Debug/E2E build from the reviewed commit, iPhone 17 Pro simulator on iOS 27.0, using disposable regtest wallets and a local Arbitrum fork.
Refuse an oversized request note and send a shorter one ✅ The long note was refused; “Coffee” reached the payer’s 1,000-sat confirmation.
J1: pasted 256 purple hearts, observed “Shorten the note and try again.” with the editor retained, then shortened the note and sent it. Closed the payer’s confirmation without paying.
j1-2x.2x.mp4
Requester on the left, payer on the right; typing and idle intervals shortened, playback at 2×.
Explain a rejected signup code ✅ The specific signup-code message appeared, Authorize remained available, and no identity was created.
J2: used an unknown signup token against the reachable test homeserver. After canceling, Profile still showed its introduction.
j2-2x.2x.mp4
Rejected signup and the unchanged profile state, at 2×.
Reject a request whose Lightning proof cannot fit ⏭️ The rc6 issuer rejected the requested proposal before delivery.
Manual Test 1: a linked issuer using Paykit 0.1.1-rc6 (9919198a) refused the 256-heart payment reference with Private Application Message (1499 bytes) exceeds pubky-noise message size (1000 bytes). That exact input could not reach the app, so the device-side refusal remains unrun. Unit tests passed for 1,000-byte and 1,001-byte proof preparation, including the Allowance pre-acceptance guard.
Keep an older USDT transfer from paying a new request ✅ The verified receipt shows “Received · Before request · 0.05 USDT,” without a paid status.
Manual Test 2: sent a real 0.05-USDT transfer on the local fork, then created a new 0.05-USD request with a fixed 1:1 USDT rate. Submitted an EIP-712 proof signed by the transfer’s EOA owner through an rc6 peer. The receipt’s block timestamp was 8 November, more than two days before the request; the timestamp and successful transfer were checked against the fork receipt. History and request details show Before request. The saved result also survived an app restart.
usdt-result-2x.2x.mp4
Verified request details, followed by the saved receipt status after restart, at 2×; the home activity also shows the older transfer date.
Explain an unreachable signup homeserver ✅ “Check your connection and try again.” appeared and the authorization sheet stayed open.
Manual Test 3: used a syntactically valid homeserver key with no resolvable record in the isolated test network, exercising the transport failure path.
network-2x.2x.mp4
Failed signup authorization and retained retry controls, at 2×.
Check: focused unit suites and static validation ✅ The changed payment, receipt, backup and authorization suites passed.
The six focused suites completed successfully on iOS 26.5 with Xcode 27.0. SwiftFormat 0.64.6 lint on the changed Swift files, translation validation and git diff --check passed.
Full review of 62db433..a1c126d, including the rc6 pin, production changes, tests and journeys. Subscription message overflow, automatic Allowance handling and receipt/backup edge cases received unit coverage; they were not separately driven through the UI. The in-place rc2 state upgrade was not device-tested.
Twin: bitkit-android#1803 fix: adopt paykit 0.1.1-rc6 matches the proof-sizing, proposal/signup error handling and USDT timing behavior in the changed production code. Android was not device-tested in this run.
Reviewed by gpt-6.1-sol-high
There was a problem hiding this comment.
Partial device gate at a1c126d. The code scan was clean in my earlier review; this adds device results. I stopped the run before it finished, so this is not an approval yet.
Environment:
- Two iOS 26.5 simulators: A is an iPhone 17 Pro, B an iPhone 17 Pro Max.
- Staging Pubky homeserver and httprelay, local regtest.
- Builds: this PR (a1c126d), master 62db433 (Paykit rc2) as the upgrade baseline, and an issuer fixture build of this PR for M1.
| Case | Result |
|---|---|
| J1 | Pass on screen. A 1000 sat request with a long "💜" note was refused on Send Request: the payer got no new request and the sheet stayed on the request. The same request with the note "Coffee" sent and showed Payment Requested. I did not capture the toast text. |
| J2 | Pass on screen. Authorize with the used signup code ended with the sheet still open, and no identity was created: Profile still shows the Portable Profile intro after Cancel. The log shows Failed to approve pubky signup. I did not capture the toast text. |
| 3 | Pass on screen. Authorize with the homeserver unreachable failed and the sheet stayed open. I did not capture the toast text. |
| Upgrade from master (rc2) | Pass. On master, A requested 1000 sats from B ("PreUpgrade"). I then installed this PR's build over master on both simulators. B opened the pre-upgrade request and paid it (Sent to GateA1237, −1143 sats including fee), and A received it. |
| 1 | Not finished. The issuer fixture build was installed on A and sent its request, but the payer's result was not checked. |
| 2 | Not run. It needs a USDT fixture. |
Evidence is kept locally in .ai/gate_1237_1794340718 (screenshots, app logs, notes).
Still to do before approval: case 1's payer result, case 2, and the toast text for J1, J2 and 3.
Reviewer model: claude-opus-5-5 (device gate and coordination)
Reasoning effort: default (Opus)
jvsena42
left a comment
There was a problem hiding this comment.
Device gate finished at a1c126d92, with the code scan clean from my earlier review. Approving.
| Case | Result |
|---|---|
| J1 | Pass. 256 pasted "💜" → "Error / Shorten the note and try again.", and nothing was sent. |
| J2 | Pass. "Authorization Failed / The homeserver did not accept this signup code. Try again or use another signup link." The sheet stayed open and no identity was created. |
| 3 | Pass. "Authorization Failed / Check your connection and try again." The sheet stayed open. I triggered it with a homeserver key that has no homeserver record, a lookup failure the PR maps to the same message. |
| 1 | Pass, with an adjusted fixture (below). The payer swiped and got "Transaction Failed / The payment request is no longer available." (Payment failed: requestUnavailable). No proof was stored, no submitPaymentProof ran, and Savings and activity were unchanged. |
| Upgrade from master (rc2) | Pass (posted earlier). |
| 2 |
Manual test 1 as written can't happen. 256 "💜" make a 1024-byte reference, so the issuer's own proposal is refused with the note message. With the stock proposal shape, 112 or more hearts are refused at propose, and 106 send and pay normally. So any reference that fits a stock proposal also fits the proof. I exercised the payer check with an issuer fixture whose proposal drops expiry, conversion, deadline and metadata and keeps only the p2wpkh endpoint: 150 hearts (600 bytes) sent and the payer refused it as above. The rail was on-chain, since neither wallet had a channel. It goes through the same PaykitPaymentProofSize.validate as Lightning. The test text would be clearer as "a reference that fits a proposal but not its proof".
- Alto rejected the user operation, because it checks the paymaster's
validUntilagainst wall time. - A wall-time paymaster was refused by Core, because
validUntilwas more than 900 s past the chain time. - Alto with
--expiration-check falseaccepted the operation, but every bundle failed withAA32 paymaster expired or not due.
Running it needs a fixture that bundles under a lagged chain clock, or a payer build that proves an older transfer. The before-request logic rests on PaykitPaymentRequestServiceTests and PaykitPaymentStateBackupTests.
Device gate: a1c126d92 — journeys: 2 passed (J1, J2); manual: 1, 3 and the upgrade passed, 2 not run (fixture); perf: propose 1.1-1.2 s; resolving a request before Confirm took 4-29 s on a host at load 11-140 with other builds running, so it is not comparable to master; iOS only (the Android twin is merged).
CI is green at this head.
Reviewer model: Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
|
@ben-kaufman Merged, as it was approved and green. |
Closes #1271
Twin: synonymdev/bitkit-android#1803
This PR:
Description
0.1.1-rc6at revision9919198a, the same way the rc2 bump did. The app compiled unchanged against rc6. Only test fixtures needed the new Payment Request record times and the acknowledged list in Private Payment List delivery reports.paymentProofEventLen, using the longest proof the rail can produce and the IDs it will send. A request whose proof would not fit the 1000-byte message limit shows "The payment request is no longer available." and nothing is paid. An Allowance checks this before it accepts the request automatically, so such a request stays manual instead of being accepted and then left unpaid. The USDT check used a hand-built copy of the message and now uses the same measurement. Bitkit's own requests use a fixed-length payment reference, so their longest proof always fits.message_too_largeshows "Shorten the note and try again." (wallet__payment_request_note_too_long, new) for a one-time request and the existing "Shorten the subscription name or description and try again." for a subscription, instead of the raw SDK error. Nothing is queued in either case.profile__auth_error_signup_code_rejected, new) and a network, 5xx, 429 or PKARR failure, which rc6 reports as a transport error, shows "Check your connection and try again." (pubky_auth__identity_unavailable_desc, new on iOS with Android's translations). Other failures keep their description.wallet__payment_request_before_request, new) and does not pay the request or Billing Period. The day covers Arbitrum timestamp lag. Receipts and backups from earlier builds keep their meaning.republishIdentityfinds no record. Platform callbacks already throwPaykitError. Bitkit uses Pubky shared state, so thestateRevisionchange does not apply. Accounting already reconciles state that requires it and automatic acceptance uses one trusted time. Requests that rc2 showed as invalid after a late Acceptance are shown from their new state. Encrypted Link sends check write slots before publishing, with no API change. The homeservers, relays and homegate Bitkit uses on staging and production chain to ISRG Root X1, whichwebpki-roots1.0.11 keeps.Out of Scope
Bitkit/Services/PaykitAllowanceManager.swift: automatic acceptance of Allowance offers still accepts each offer separately.acceptPendingAllowanceProposalswould also accept a contact's offers in assets Bitkit does not list and cannot bind each acceptance to the offer Bitkit listed.Bitkit/Services/PubkyService.swift: deleting a single contact still blocks the peer when the private list withdrawal reports a failure, as before.Bitkit/Services/PaykitUsdtPaymentService.swift: the one-day tolerance covers Arbitrum timestamp lag. It does not add a margin for the payee's own clock skew.Design
N/A — no design available. The change adds text to existing toasts and to the USDT receipt status.
Preview
N/A
QA Notes
Setup
journeys/payment-requests/README.md(Request summary setup). No funds are needed.journeys/pubky-auth/README.md). Complete one signup_grant URL on another wallet first, then use the same URL, whose token is now used, for the journey.Journeys
request-note-too-long.xml— a note that cannot fit one Paykit message is refused with "Shorten the note and try again." before anything is sent, and the shortened request sendsgrant-signup-rejected-code.xml— a refused signup code shows the signup code message, the sheet stays open and no identity is createdManual Tests
Automated Checks
PaykitPaymentProofServiceTests.swift— Lightning and on-chain preparation accepts a request whose proof is exactly 1000 bytes and refuses one at 1001 bytes without storing anything, and the Allowance ID counts toward the sizePaykitAllowanceExecutorTests.swift— a request whose proof cannot fit stays manual with no execution claim, Acceptance or paymentPaykitRequestPricingTests.swift— the USDT placeholder is a valid proof of the longest shape, the USDT check accepts exactly 1000 bytes and refuses 1001, the Billing Period and quote ID count toward the size, and Bitkit's own requests leave room for their longest proofPaykitPaymentRequestServiceTests.swift—message_too_largebecomes the note and subscription messages with nothing proposed, and other protocol errors pass through. A USDT transfer more than a day before the request record time, in Bitkit's or the SDK's timestamp format, is flagged, shown as Before request ahead of other labels and does not pay it, while one within the day or without a valid record time doesPaykitPaymentStateBackupTests.swift— the before-request flag survives a backup, and an older backup or a receipt saved before the flag existed still counts as paidPubkyAuthApprovalSheetTests.swift— a refused signup code and a transport error get their messages, other errors keep their ownPaykitAllowanceTests.swift— Allowance fixtures use canonical UUID-v4 IDs, which the SDK requires in a Payment Proofopenssl s_clientagainst a bundle built fromwebpki-root-certs1.0.11 — homeserver, httprelay, homegate and nexus onpubky.appandstaging.pubky.app,pkarr.pubky.app,pkarr.pubky.organdrelay.pkarr.orgverify, ending at ISRG Root X1Evidence
60111132, a throwaway iPhone 17 simulator (iOS 26.5), Xcode 27.2: CI's unit test selection (xcodebuild test, skipping the integration suites as inunit-tests.yml) passed,BitkitTests3189 executed with 1 skipped (UsdtNativeRecoveryIntegrationTests, needs a local fixture) andBitkitUITests44 executed with 4 skipped, 0 failures. The same selection also passed on265389e8(the bump alone, 3176 executed) andcd4236ab(3186 executed). Head1594952eonly renames the changelog fragment.node scripts/validate-translations.jsreports 0 errors with the same warnings as master, andgit diff --checkis clean.Paykit.xcframework.zipfor v0.1.1-rc6 matches thePackage.swiftchecksum23bf9612c2bc7579dee8a40b18527936ba1a0531f788e13f9aae8f6286ccf3f5.Gaps
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