Skip to content
Open
23 changes: 23 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# CodeRabbit Configuration
# Documentation: https://docs.coderabbit.ai/guides/configure-coderabbit

language: en-US

reviews:
# Automatically review new PRs and incremental pushes
auto_review:
enabled: true
drafts: false
base_branches:
- master
- develop

# Review profile - chill for balanced feedback
profile: chill

# Request changes when issues are found
request_changes_workflow: true

chat:
# Allow interaction with CodeRabbit via comments
auto_reply: true
211 changes: 211 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# DashSync iOS

DashSync is a lightweight blockchain client library for iOS/macOS that enables applications to interact with the Dash cryptocurrency network. It supports both Dash Core Network (Layer 1) and Dash Platform (Layer 2).

## Quick Reference

- **Language**: Objective-C with C/C++/Rust interop
- **Build System**: Xcode + CocoaPods
- **Deployment**: iOS 13.0+, macOS 10.15+
- **Pod Name**: `DashSyncPod`

## Build Requirements

```bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim

# Install protobuf and grpc
brew install protobuf grpc cmake
```

## Common Commands

```bash
# Run example project
cd Example && pod install && open DashSync.xcworkspace

# Run tests
cd Example && xcodebuild test -workspace DashSync.xcworkspace -scheme DashSync-Example -destination 'platform=iOS Simulator,name=iPhone 15'

# Update pods
cd Example && pod update
```

## Project Structure

```
DashSync/
├── DashSync/shared/ # Main framework source (cross-platform)
│ ├── Models/ # Core domain models (24 subdirectories)
│ ├── Libraries/ # Utility libraries
│ └── DashSync.xcdatamodeld/ # Core Data model (83 entities)
├── DashSync/iOS/ # iOS-specific code
├── DashSync/macOS/ # macOS-specific code
├── Example/ # Reference app and tests
├── Scripts/ # Build utilities
└── ChainResources/ # Blockchain data files
```
Comment on lines +38 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Specify language identifier for project structure code block.

The fenced code block for the directory structure should declare a language (e.g., text or plaintext) for markdown linting consistency.

📝 Proposed fix
-```
+```text
 DashSync/
 ├── DashSync/shared/           # Main framework source (cross-platform)
 │   ├── Models/                # Core domain models (24 subdirectories)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
DashSync/
├── DashSync/shared/ # Main framework source (cross-platform)
│ ├── Models/ # Core domain models (24 subdirectories)
│ ├── Libraries/ # Utility libraries
│ └── DashSync.xcdatamodeld/ # Core Data model (83 entities)
├── DashSync/iOS/ # iOS-specific code
├── DashSync/macOS/ # macOS-specific code
├── Example/ # Reference app and tests
├── Scripts/ # Build utilities
└── ChainResources/ # Blockchain data files
```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

49-49: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @CLAUDE.md around lines 38 - 49, Add a language identifier to the fenced code
block that contains the DashSync directory tree (the block starting with
"DashSync/") so markdown linters recognize it as plain text; update the opening
triple-backticks to include a language token such as "text" or "plaintext"
(e.g., change ``` to ```text) while keeping the directory tree content
unchanged.


## Architecture

### Two-Layer Design
- **Layer 1 (Core)**: Traditional blockchain - transactions, blocks, masternodes
- **Layer 2 (Platform)**: Decentralized apps - identities, documents, contracts

### Model-Manager Pattern
- **Models**: Data structures (`DSChain`, `DSWallet`, `DSTransaction`)
- **Managers**: Service coordinators (`DSChainManager`, `DSPeerManager`)

### Key Managers
| Manager | Purpose |
|---------|---------|
| `DSChainsManager` | Multi-chain coordinator (singleton) |
| `DSChainManager` | Single chain operations |
| `DSPeerManager` | P2P network connectivity |
| `DSTransactionManager` | Transaction pool |
| `DSMasternodeManager` | Masternode lists & quorums |
| `DSIdentitiesManager` | Blockchain identities |
| `DSGovernanceSyncManager` | Governance data sync |

### Persistence
- **Core Data** with SQLite backend
- 83 entity definitions in `DashSync.xcdatamodeld`
- Custom transformers in `Models/Persistence/Transformers/`

## Code Conventions

### Naming
- All classes prefixed with `DS` (e.g., `DSChain`, `DSWallet`)
- Entities suffixed with `Entity` (e.g., `DSChainEntity`)
- Managers suffixed with `Manager` (e.g., `DSPeerManager`)

### File Organization
- Public headers in main directory
- `+Protected.h` files for subclass-accessible interfaces
- Categories in `Categories/` subdirectories

### Notifications
Event-driven via `NSNotificationCenter`:
- `DSChainBlocksDidFinishSyncingNotification`
- `DSWalletBalanceDidChangeNotification`
- `DSPeerManagerConnectedPeersDidChangeNotification`

## Key Classes

### Chain & Sync
- `DSChain` (3,562 lines) - Central blockchain state manager
- `DSBlock`, `DSMerkleBlock` - Block representations
- `DSChainLock` - Chain lock mechanism

### Wallet
- `DSWallet` - HD wallet management
- `DSAccount` - Account within wallet
- `DSBIP39Mnemonic` - Mnemonic seed handling
- `DSDerivationPath` - BIP32/44 key derivation

### Transactions
- `DSTransaction` - Base transaction class
- `DSCoinbaseTransaction` - Mining rewards
- `DSProviderRegistrationTransaction` - Masternode registration
- `DSQuorumCommitmentTransaction` - Quorum operations
- `DSCreditFundingTransaction` - Platform funding

### Identity & Platform
- `DSBlockchainIdentity` - Dash Platform identity
- `DSBlockchainInvitation` - Contact requests
- `DPContract` - Platform smart contracts
- `DPDocument` - Platform documents

### Privacy
- `DSCoinJoinManager` - CoinJoin mixing coordination
- `DSCoinJoinWrapper` - Protocol implementation

## Network Support

| Network | Purpose |
|---------|---------|
| Mainnet | Production Dash network |
| Testnet | Testing environment |
| Devnet | Development chains |
| Regnet | Local regression testing |

## Testing

Tests located in `Example/Tests/`:
- `DSChainTests.m` - Chain operations
- `DSTransactionTests.m` - Transaction handling
- `DSDeterministicMasternodeListTests.m` - Masternode lists
- `DSCoinJoinSessionTest.m` - Privacy mixing
- `DSDIP14Tests.m` - DIP compliance

## CI/CD Workflows

- `build.yml` - Main CI pipeline
- `test.yml` - Unit tests
- `lint.yml` - Code linting
- `coverage.yml` - Code coverage
- `syncTestMainnet.yml` / `syncTestTestnet.yml` - Network sync tests

## Dependencies

Key CocoaPods:
- **DashSharedCore** - Rust-based cryptographic primitives
- **CocoaLumberjack** - Logging framework
- **DAPI-GRPC** - Decentralized API protocol
- **TinyCborObjc** - CBOR serialization

## Localization

Supports 15+ languages: en, de, es, ja, zh-Hans, zh-Hant-TW, uk, bg, el, it, cs, sk, ko, pl, tr, vi

## Development Workflow

### Git Workflow Policy

**NEVER commit or push changes without explicit user permission.**

When the user asks you to make code changes:
1. Make the requested changes to the code
2. Show what was changed (using `git diff` or explanation)
3. **STOP and WAIT** for explicit permission to commit/push
4. Only commit/push when the user explicitly says to do so

**Example phrases that give permission to commit/push:**
- "commit these changes"
- "push to github"
- "create a commit and push"
- "commit and push all changes"

**Do NOT commit/push** just because the user asked for code changes. They may want to test first.

#### Permission Does NOT Carry Over

**Each set of changes requires its own explicit permission.** If the user gave permission to commit earlier in the conversation, that permission applies ONLY to those specific changes - NOT to any subsequent changes.

**Example scenario:**
1. User: "Fix bug X, then commit and push" → Permission granted for bug X fix only
2. User: "Now fix bug Y" → Make the fix, show diff, **STOP AND WAIT** - no permission to commit yet
3. User: "Looks good, commit it" → NOW permission is granted for bug Y fix

**Common mistake to avoid:** After completing a task like "address review comments" or "fix these issues", do NOT automatically commit. The user needs to test the changes first. Always pause after showing the diff and wait for explicit commit instruction.

#### Testing Before Commit

This is especially important for DashSync because:
- Changes affect downstream projects (dashwallet-ios, other consumers)
- Logging and behavioral changes need runtime verification
- The user must run the code to confirm changes work as expected

Always wait for the user to confirm they have tested and verified the changes before committing.

### Related Repositories
- **DashJ** (Android equivalent): https://github.com/dashpay/dashj
- **Dash Wallet Android**: https://github.com/dashpay/dash-wallet

## External Resources

- [Dash Core Specs](https://dashcore.readme.io/docs)
- [Dash Improvement Proposals](https://github.com/dashpay/dips)
- [Developer Discord](https://discord.com/channels/484546513507188745/614505310593351735)
11 changes: 0 additions & 11 deletions DashSync/shared/Categories/NSData/NSData+Dash.m
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,13 @@ BOOL setKeychainData(NSData *data, NSString *key, BOOL authenticated) {
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)item, NULL);

if (status == noErr) return YES;
DSLogPrivate(@"SecItemAdd error: %@", [NSError osStatusErrorWithCode:status].localizedDescription);
return NO;
}

if (!data) {
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);

if (status == noErr) return YES;
DSLogPrivate(@"SecItemDelete error: %@", [NSError osStatusErrorWithCode:status].localizedDescription);
return NO;
}

Expand All @@ -72,7 +70,6 @@ BOOL setKeychainData(NSData *data, NSString *key, BOOL authenticated) {
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);

if (status == noErr) return YES;
DSLogPrivate(@"SecItemUpdate error: %@", [NSError osStatusErrorWithCode:status].localizedDescription);
return NO;
}

Expand All @@ -89,7 +86,6 @@ BOOL hasKeychainData(NSString *key, NSError **error) {

if (status == errSecItemNotFound) return NO;
if (status == noErr) return YES;
DSLogPrivate(@"SecItemCopyMatching error: %@", [NSError osStatusErrorWithCode:status].localizedDescription);
if (error) *error = [NSError osStatusErrorWithCode:status];
return NO;
}
Expand All @@ -104,7 +100,6 @@ BOOL hasKeychainData(NSString *key, NSError **error) {

if (status == errSecItemNotFound) return nil;
if (status == noErr) return CFBridgingRelease(result);
DSLogPrivate(@"SecItemCopyMatching error: %@", [NSError osStatusErrorWithCode:status].localizedDescription);
if (error) *error = [NSError osStatusErrorWithCode:status];
return nil;
}
Expand Down Expand Up @@ -164,9 +159,6 @@ BOOL setKeychainDict(NSDictionary *dict, NSString *key, BOOL authenticated) {
]];
set = [set setByAddingObjectsFromArray:classes];
NSDictionary *dictionary = [NSKeyedUnarchiver unarchivedObjectOfClasses:set fromData:d error:error];
if (*error) {
DSLogPrivate(@"error retrieving dictionary from keychain %@", *error);
}
return dictionary;
//}
}
Expand All @@ -189,9 +181,6 @@ BOOL setKeychainArray(NSArray *array, NSString *key, BOOL authenticated) {
]];
set = [set setByAddingObjectsFromArray:classes];
NSArray *array = [NSKeyedUnarchiver unarchivedObjectOfClasses:set fromData:d error:error];
if (*error) {
DSLogPrivate(@"error retrieving array from keychain %@", *error);
}
return array;
}
}
Expand Down
2 changes: 0 additions & 2 deletions DashSync/shared/Categories/NSManagedObject+Sugar.m
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ + (NSArray *)fetchObjects:(NSFetchRequest *)request inContext:(NSManagedObjectCo

[context performBlockAndWait:^{
a = [context executeFetchRequest:request error:&error];
if (error) DSLog(@"%s: %@", __func__, error);
}];

return a;
Expand Down Expand Up @@ -277,7 +276,6 @@ + (NSUInteger)countObjects:(NSFetchRequest *)request inContext:(NSManagedObjectC

[context performBlockAndWait:^{
count = [context countForFetchRequest:request error:&error];
if (error) DSLog(@"%s: %@", __func__, error);
}];

return count;
Expand Down
13 changes: 12 additions & 1 deletion DashSync/shared/Categories/NSManagedObjectContext+DSSugar.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
//

#import "DSDataController.h"
#import "DSLogger.h"
#import "NSManagedObjectContext+DSSugar.h"


Expand Down Expand Up @@ -66,20 +67,30 @@ - (NSError *)ds_saveInBlockAndWait {

- (NSError *)ds_save {
if (!self.hasChanges) return nil;

NSTimeInterval saveStart = [[NSDate date] timeIntervalSince1970];
NSUInteger insertedCount = self.insertedObjects.count;
NSUInteger updatedCount = self.updatedObjects.count;
NSUInteger deletedCount = self.deletedObjects.count;

#if TARGET_OS_IOS
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
}];
#endif
NSError *error = nil;
if (![self save:&error]) { // persist changes
DSLog(@"%s: %@", __func__, error);
#if DEBUG
abort();
#endif
}
#if TARGET_OS_IOS
[[UIApplication sharedApplication] endBackgroundTask:taskId];
#endif

NSTimeInterval saveTime = ([[NSDate date] timeIntervalSince1970] - saveStart) * 1000.0;
DSLogInfo(@"CoreData", @"Save completed in %.1f ms (inserted: %lu, updated: %lu, deleted: %lu)",
saveTime, (unsigned long)insertedCount, (unsigned long)updatedCount, (unsigned long)deletedCount);

return error;
Comment on lines 68 to 94
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Logging occurs even on save failure.

The timing and counts are captured before the save, which is correct since Core Data clears these sets after save. However, the log at lines 91-92 will report "Save completed" even if the save failed (in release builds where abort is not called).

🐛 Proposed fix to only log on success
     NSError *error = nil;
     if (![self save:&error]) { // persist changes
 `#if` DEBUG
         abort();
 `#endif`
+        return error;
     }
 `#if` TARGET_OS_IOS
     [[UIApplication sharedApplication] endBackgroundTask:taskId];
 `#endif`

     NSTimeInterval saveTime = ([[NSDate date] timeIntervalSince1970] - saveStart) * 1000.0;
     DSLogInfo(@"CoreData", @"Save completed in %.1f ms (inserted: %lu, updated: %lu, deleted: %lu)",
               saveTime, (unsigned long)insertedCount, (unsigned long)updatedCount, (unsigned long)deletedCount);

     return error;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- (NSError *)ds_save {
if (!self.hasChanges) return nil;
NSTimeInterval saveStart = [[NSDate date] timeIntervalSince1970];
NSUInteger insertedCount = self.insertedObjects.count;
NSUInteger updatedCount = self.updatedObjects.count;
NSUInteger deletedCount = self.deletedObjects.count;
#if TARGET_OS_IOS
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
}];
#endif
NSError *error = nil;
if (![self save:&error]) { // persist changes
DSLog(@"%s: %@", __func__, error);
#if DEBUG
abort();
#endif
}
#if TARGET_OS_IOS
[[UIApplication sharedApplication] endBackgroundTask:taskId];
#endif
NSTimeInterval saveTime = ([[NSDate date] timeIntervalSince1970] - saveStart) * 1000.0;
DSLogInfo(@"CoreData", @"Save completed in %.1f ms (inserted: %lu, updated: %lu, deleted: %lu)",
saveTime, (unsigned long)insertedCount, (unsigned long)updatedCount, (unsigned long)deletedCount);
return error;
- (NSError *)ds_save {
if (!self.hasChanges) return nil;
NSTimeInterval saveStart = [[NSDate date] timeIntervalSince1970];
NSUInteger insertedCount = self.insertedObjects.count;
NSUInteger updatedCount = self.updatedObjects.count;
NSUInteger deletedCount = self.deletedObjects.count;
`#if` TARGET_OS_IOS
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
}];
`#endif`
NSError *error = nil;
if (![self save:&error]) { // persist changes
`#if` DEBUG
abort();
`#endif`
}
`#if` TARGET_OS_IOS
[[UIApplication sharedApplication] endBackgroundTask:taskId];
`#endif`
if (error) return error;
NSTimeInterval saveTime = ([[NSDate date] timeIntervalSince1970] - saveStart) * 1000.0;
DSLogInfo(@"CoreData", @"Save completed in %.1f ms (inserted: %lu, updated: %lu, deleted: %lu)",
saveTime, (unsigned long)insertedCount, (unsigned long)updatedCount, (unsigned long)deletedCount);
return error;
}
🤖 Prompt for AI Agents
In `@DashSync/shared/Categories/NSManagedObjectContext`+DSSugar.m around lines 68
- 94, ds_save currently logs "Save completed" and timing/counts unconditionally
even when the save (the -[NSManagedObjectContext save:&error] call in ds_save)
fails; modify ds_save so that the DSLogInfo call (and the computed
saveTime/insertedCount/updatedCount/deletedCount logging) occurs only when
save:&error returns YES (i.e., after a successful save), while still ensuring
the background task is ended and the NSError is returned on failure; locate the
save call and DSLogInfo in ds_save and wrap the logging in the success branch so
failures do not produce a "Save completed" message.

}

Expand Down
Loading
Loading