refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

refactor: integrate paykit sdk - #606

Merged
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration
Jul 9, 2026
Merged

refactor: integrate paykit sdk#606
piotr-iohk merged 18 commits into
masterfrom
codex/paykit-sdk-native-integration

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Replaces Bitkit's custom Paykit private/public payment plumbing with the native Paykit SDK.
  2. Pins Paykit to the published v0.1.0-rc23 Swift package release.
  3. Moves Pubky profile, contact, public endpoint, private endpoint, and SDK backup state handling through SDK APIs while keeping wallet execution and UI mapping in Bitkit.
  4. Keeps public fallback, Ring/public-only capability handling, contact attribution, and receiving-detail rotation behavior covered by the app layer.
  5. Adds hardening for auth approval capabilities, canceled Ring auth cleanup, profile label fallback, pending private drain retries, and best-effort profile delete/sign-out cleanup.

Linked Issues/Tasks

N/A

Screenshot / Video

N/A - SDK integration; no UI layout change.

QA Notes

Manual Tests

  • 1. Profile -> create/edit profile -> add contact by Pubky key: contact profile resolves and remains visible after app restart.
  • 2. Send -> Contact -> select contact -> complete payment: private Paykit is attempted first and public endpoints remain available as fallback when private capability is unavailable.
  • 3. Backup/restore -> restore a wallet with Pubky state -> open contacts/pay contact: SDK state restores and contact payment preparation works.
  • 4. Settings -> Payment Preference -> toggle public/private contact payments: endpoint publication state follows the selected preferences.
  • 5. Profile -> Sign Out / Delete Profile: private cleanup is attempted first, then local Pubky and SDK state clear on successful sign-out/delete.

Automated Checks

  • xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/PubkyModelTests -only-testing:BitkitTests/PubkyAuthRequestTests -only-testing:BitkitTests/PubkyProfileManagerTests -only-testing:BitkitTests/PrivatePaykitServiceTests -derivedDataPath /private/tmp/bitkit-ios-paykit-rc23-dd test -quiet passed.
  • swiftformat passed on the touched Swift files.
  • git diff --check passed.

@ben-kaufman
ben-kaufman marked this pull request as ready for review June 24, 2026 11:42
@greptile-apps

greptile-appsBot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces Bitkit's hand-rolled Paykit payment plumbing (noise-encrypted links, manual homeserver reads/writes, bespoke snapshot serialisation) with the native Paykit SDK (v0.1.0-rc21), delegating contact management, endpoint publication, private-payment list sync, backup export/import, and auth flows to SDK APIs while keeping wallet execution, invoice rotation, and UI mapping in the app.

  • PubkyService / PaykitSdkService: All PaykitFFI free-function calls are replaced by a new PaykitSdkService actor backed by atomic keychain blob storage (PaykitSdkStateBlobStore with revision-conflict detection), a session provider, and a custom PaykitSdkOperationLock that prevents interleaving across async suspension points.
  • Contacts & profiles: ContactsManager now reads/writes contacts via SDK contactRecords / saveContact / removeContact; richer per-contact metadata (bio, image, links, tags) is stored as local contactProfileOverrides backed up in MetadataBackupV1 rather than on the homeserver.
  • Backup/restore: WalletBackupV1 replaces the per-contact link-snapshot map with a single opaque paykitSdkBackupState string exported by the SDK; restore is deferred until after all backup categories are loaded \u2014 but the non-nil case is missing a local error guard (see inline comment).

Confidence Score: 3/5

Safe to land after fixing the BackupService error handling gap; the rest of the SDK integration is well-structured.

The restore path is missing a local error guard for the non-nil SDK backup state case. A corrupted or version-mismatched blob causes restoreBackup to throw, which propagates to the outer catch and silently skips the blocktank restore and PIN-reset steps. Because this touches the wallet restore flow — the path users depend on after loss or device migration — a failure here can leave the wallet in a partially-restored state. The rest of the SDK integration (actor isolation, atomic keychain writes with revision checking, auth cancellation via requestID, public payment fallback guarded against CancellationError) is well-structured.

Bitkit/Services/BackupService.swift lines 249-261 (missing local do/catch for non-nil SDK backup restore). Bitkit/Managers/ContactsManager.swift updateContact (richer profile fields now depend on MetadataBackupV1 surviving restore).

Important Files Changed

FilenameOverview
Bitkit/Services/BackupService.swiftRestore flow deferred Paykit SDK restore after wallet category; non-nil backup string throws are not caught locally, aborting blocktank and PIN-reset steps on a bad SDK blob.
Bitkit/Services/PubkyService.swiftMajor refactor: replaced PaykitFFI free-function calls with the new PaykitSdkService actor. Introduces PaykitSdkStateBlobStore (atomic keychain persistence with revision checking), PaykitSdkSessionProvider, and PaykitSdkOperationLock. Auth-cancel race is handled correctly via requestID comparison inside completeAuth.
Bitkit/Services/PrivatePaykitService+Payments.swiftPrivate/public payment resolution simplified: delegates to SDK prepareAndResolveContactPayment; cached endpoints and stale-lightning-hash eviction logic retained at app layer. Public fallback on SDK error is correctly guarded against CancellationError.
Bitkit/Services/PrivatePaykitService+Backup.swiftBackup now exports a single SDK blob string and restores via PaykitSdkService; the old per-contact link snapshot serialisation is removed.
Bitkit/Managers/ContactsManager.swiftContact CRUD now goes through SDK contactRecords/saveContact/removeContact APIs. updateContact stores bio/image/links/tags as a local contactProfileOverride rather than on the homeserver; these are only recoverable via the MetadataBackupV1 category.
Bitkit/Services/PrivatePaykitService+Endpoints.swiftEndpoint publication simplified: buildLocalEndpoints now @mainactor to allow synchronous walletHasUsableChannels access; syncLocalEndpointPublicationLocked batches reservation updates for all contacts in one SDK call.
Bitkit/Services/PrivatePaykitService+Contacts.swiftContact preparation delegates to syncLocalEndpointPublication; profile-recovery re-establishment logic removed (now handled by SDK).
Bitkit/Models/BackupPayloads.swiftWalletBackupV1 replaces per-contact link snapshots with a single optional paykitSdkBackupState string; MetadataBackupV1 gains pubkyContactProfileOverrides for local contact customisations.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI
participant PrivatePaykitService
participant PaykitSdkService
participant PaykitSdk
participant Keychain
Note over UI,Keychain: Payment preparation flow
UI->>PrivatePaykitService: beginSavedContactPayment(publicKey)
PrivatePaykitService->>PaykitSdkService: identityStatus()
PaykitSdkService->>PaykitSdk: identityStatus()
PaykitSdk-->>PaykitSdkService: IdentityStatus (privateLinkCapable)
PaykitSdkService-->>PrivatePaykitService: status
PrivatePaykitService->>PaykitSdkService: syncPrivatePaymentListsWithReservations(updates)
PaykitSdkService->>Keychain: saveStateBlobAtomically (revision-checked)
PaykitSdkService-->>PrivatePaykitService: PrivatePaymentListDeliveryReport
PrivatePaykitService->>PaykitSdkService: prepareAndResolveContactPayment(counterparty)
PaykitSdkService->>PaykitSdk: prepareAndResolveContactPayment(...)
PaykitSdk-->>PaykitSdkService: PreparedContactPayment
PaykitSdkService-->>PrivatePaykitService: resolution
alt private endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest)
else public endpoints available
PrivatePaykitService-->>UI: .opened(paymentRequest via public)
else no endpoints
PrivatePaykitService-->>UI: .noEndpoint
end
Note over UI,Keychain: Backup restore flow
UI->>BackupService: restore()
BackupService->>BackupService: performRestore(.wallet) sets pendingPaykitSdkBackupState
BackupService->>BackupService: performRestore(.metadata) restoreContactProfileOverrides
BackupService->>PrivatePaykitService: restoreBackup(pendingPaykitSdkBackupState)
PrivatePaykitService->>PaykitSdkService: restoreBackupState(blob)
PaykitSdkService->>Keychain: saveStateBlobAtomically
PaykitSdkService-->>PrivatePaykitService: ok / throws
Loading

Comments Outside Diff (1)

  1. Bitkit/Managers/ContactsManager.swift, line 355-361 (link)

    P2Richer contact profile data (bio, image, links, tags) is now only stored locally

    Previously, updateContact serialised the full PubkyProfileData (name, bio, image URL, links, tags) to the homeserver. Now it calls PubkyService.saveContact with only label: name and stores the remaining fields in a local contactProfileOverride. The override is backed up via MetadataBackupV1.pubkyContactProfileOverrides, but if the metadata backup is absent or corrupted while the wallet backup is intact, all bio/image/links/tag customisations are silently lost on restore. This is a silent data-availability regression compared to the old homeserver-backed approach.

Reviews (1): Last reviewed commit: "chore: polish paykit cleanup" | Re-trigger Greptile

Comment threadBitkit/Services/BackupService.swift

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fbaa310479

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadBitkit/Managers/PubkyProfileManager.swift
Comment threadBitkit/Managers/PubkyProfileManager.swift
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

For the outside-diff contact override note: this is intentional with the SDK path. Edited contact details are app-local now and backed up in metadata. We do not want to publish bio/image/links/tags back to the homeserver. If metadata backup is missing or corrupt, those local customizations are lost like other metadata, but wallet backup should not own that app-local contact data.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 51b7c2ce.

This now uses Paykit v0.1.0-rc23, which includes the SDK-side recovery fix for recovery-required stale encrypted-link state after profile delete/recreate. App-side I also made profile delete/sign-out private cleanup best-effort so PrivateUnavailable no longer blocks the user, merged pending private drain retry keys with a generation guard, forwarded auth URL capabilities, cleared completed Ring auth sessions when the app flow is canceled, and added contact-label fallback for blank SDK profiles.

Checked:

  • focused iOS Paykit/Pubky tests with xcodebuild
  • swiftformat
  • git diff --check

Comment threadBitkit/Services/PubkyService.swift Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retest - pls see: synonymdev/bitkit-android#1040 (comment)

piotr-iohk
piotr-iohk previously approved these changes Jul 7, 2026

@piotr-iohkpiotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jvsena42
jvsena42 previously approved these changes Jul 8, 2026

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with two nits

Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment threadBitkit/Services/PrivatePaykitService+Payments.swift Outdated
@piotr-iohk
piotr-iohk enabled auto-merge July 9, 2026 07:41
@piotr-iohkpiotr-iohk added this to the 2.4.0 milestone Jul 9, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from jvsena42 and piotr-iohk via 3be3ee4July 9, 2026 09:34
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk@jvsena42 just need re approval please after last commit removing dead code.

@piotr-iohk
piotr-iohk merged commit 9a3b00e into masterJul 9, 2026
11 checks passed
@piotr-iohk
piotr-iohk deleted the codex/paykit-sdk-native-integration branch July 9, 2026 12:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ben-kaufman@piotr-iohk@Jasonvdb@jvsena42