-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rust: SargonOS
&Drivers
more Profile Logic | Swift: Shared State (working example app)!
#131
Conversation
Because Writing need be async sync writing to secure storage must be async in Android. It's probably best to not try something like: struct SargonKey: PersistenceKey {
...
func save(_ value: Value) {
Task {
try await SargonOS.shared.save(value)
}
}
} Because I think this might get out of sync, what if saving failed? I think the But you are most welcome to try it yourself and prove me wrong :) I'm would love to be wrong about that! @kugel3 also note that the SharedReader is not In Swift Sargon - it is in the small example app, but should be made production ready by iOS team and maybe moved into Swift Sargon, behind a |
@matiasbzurovski I think I have addressed all PR comments and applied all relevant suggestions. Thanks for through review, have a look and if it is to your satisfaction - and if you dont have any suggestions on alternative analogy to Bios, Drivers and OS - approve? 😊 |
src/system/drivers/event_bus_driver/support/event_profile_modified.rs
Outdated
Show resolved
Hide resolved
public static let shared = Keychain(service: "works.rdx.planbok") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, and that's exactly what we are doing actually 🙂 We don't need the shared
Keychain instance on iOS side, if you delete it it would still compile as it isn't used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for tackling/answer all my comments. Added some minor things and unresolved some comments that I don't think have been resolved. Approving anyway as they are all minor, great work 👏
examples/iOS/Backend/Sources/Planbok/SharedState/SargonKey.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tremendous work 🥇 💯 , left some comments that I would like to clarify.
src/system/drivers/event_bus_driver/support/test/rust_event_bus_driver.rs
Show resolved
Hide resolved
|
||
#[uniffi::export(with_foreign)] | ||
#[async_trait::async_trait] | ||
pub trait HostInfoDriver: Send + Sync + std::fmt::Debug { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason why there are multiple functions instead of one that provides the full model?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the granularity is good I think! But sure could have been get_device_info()
single function. Hmm I dont feel strongly about it either way :)
let (profile, bdfs) = | ||
Self::create_new_profile_with_bdfs(&clients, bdfs_mnemonic) | ||
.await?; | ||
|
||
secure_storage.save_private_hd_factor_source(&bdfs).await?; | ||
|
||
secure_storage | ||
.save_profile_and_active_profile_id(&profile) | ||
.await?; | ||
|
||
info!("Saved new Profile and BDFS, finish booting SargonOS"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like it should be part of ProfileHolder.
Then, in general, IMO, I would expect SargonOS to not have much logic in its functions, it should ideally just forward requests to the respective subcomponents.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which part specifically do you think should move to ProfileHolder
? ProfileHolder
does NOT have the clients, nor do I think it should, it should only "hold the Profile". But yes we might wanna introduce one more layer though!
CURRENT (current state of this PR):
SargonOS(HAS ALL clients) -> ProfileHolder(NO clients) -> Profile
ONE MORE LAYER:
SargonOS(HAS ALL clients) -> ProfileManager(HAS SOME clients) -> ProfileHolder(NO clients) -> Profile
But maybe something that can be done later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am also asking this in the context of what would be the standard, should only SargonOS operate with clients, or subcomponents can have clients?
I would add RadixConnectMobile which would need some clients.
As for having one more layer, ProfileHolder by itself doesn't seem quite useful, is there a reason I am missing as to why not have just ProfileManager that operates on the Profile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has to do with Rust and UniFFI :) SargonOS
cannot UniFFI export *mut self
methods. So we cannot use mut
. Thus SargonOS MUST have "interior mutability". But UniFFI cannot use RefCell
(or Cell
) so we MUST use RwLock
. And working with that is .... a bit cumbersome, so that code I dont want in SargonOS
, but it has to go somewhere, that is the purpose of ProfileHolder
! To be able to AT ALL mutate a Profile
and still work with UniFFI :)
RadixConnectMobile
is probably going to be a client! It might be a client which uses other clients - which would be a first. So maybe we should introduce one new "classifications"? Maybe "component"? So we call it RCMComponent
and we say: "'Components' are allowed to have 'Clients'" - and then we call ProfileManager
ProfileManagerComponent
?
I think that would work quite well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RadixConnectMobile is probably going to be a client
Isn't client reserved to drivers? I don't expect Wallets to have to init a RadixConnectMobile client, so with your terminology it would be more of a component, similar to ProfileManager.
As Sargon would grow, we will have more and more components for the sake of encapsulation.
Probably Module
would be a better term in the context of OS, but might be confusing in the context of Cargo :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but wouldn't that be too much to put a holder around whole SargonOS? does it mean that a potential RadixConnectMobile request might be blocked by some unrelated Profile change?
Profile is quite likely to be accessed concurrently with MFA(also potentially with cloud backups done in background), so we should start thinking about it. An option seems to be using actor model in Sargon directly with actix, although it is likely to make the read/writes async.
Additionally, we are already using actor model for ProfileStore in iOS codebase, any reason that we don't need the actor functionality anymore with SargonOS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true, Im not sure what we wanna do. if we can find truely distinct components, where RadixConnectMobile does not use Profile at all then it is easily built. We need to give it some thought. And while giving it thought, we need to eval "current state" and "after having moved to Workspace and only One crate uses UniFFI" scenarios. The former prevents us from using actix
actors, since they are not UniFFI compatible (and probably very hard to be made UniFFI compatible).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for actors not working with UniFFI when we want it to be fully private to SargonOS? You say that this:
#[derive(Debug, uniffi::Object)]
struct SargonOS {
profile_manager: Actor
}
cannot be exposed as an object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wrong! We can! Works with uniffi::Object
indeed! - But does not work with uniffi::Record
... I'm too used to thinking about records...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing then! We can pursue using Actors, hopefully it works smoothly 🤞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Impressive work! 👏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be a huge win for the wallets when we migrate to Sargon! 💯
I still keep an eye on your discussion with Ghenadie.
Merged into temporary branch |
* Rust: `SargonOS` &`Drivers` more Profile Logic | Swift: Shared State (working example app)! (#131) Introducing SargonOS * MFA: Sec Structure of Factor <Source | ID | Instance>s (#150) MFA Batch 1: SecurityStructureOfFactorSources and MatrixOfFactorSoures and also on FactorSourceID and FactorInstance level. Also add some new FactorSourceKinds at least a POC version of them or stubbed out ones. * Sargon OS undo Profile JSON format change (#177) Revert Profile JSON format changes in DeviceInfo * Merge 'main' into 'sargon_os' * Implement HostInfo struct (#189) * Implement HostInfo struct * Store host id in secure storage * Fix doc * Separate id and date from host info * Implement HostOS that will encapsulate the different values and constants of each host os * Expose uniffi method for creating device info from host info * Small fix for AppleHostDriver (#192) wip --------- Co-authored-by: micbakos-rdx <[email protected]> Co-authored-by: micbakos-rdx <[email protected]> Co-authored-by: Ghenadie <[email protected]>
* Rust: `SargonOS` &`Drivers` more Profile Logic | Swift: Shared State (working example app)! (#131) Introducing SargonOS * MFA: Sec Structure of Factor <Source | ID | Instance>s (#150) MFA Batch 1: SecurityStructureOfFactorSources and MatrixOfFactorSoures and also on FactorSourceID and FactorInstance level. Also add some new FactorSourceKinds at least a POC version of them or stubbed out ones. * Sargon OS undo Profile JSON format change (#177) Revert Profile JSON format changes in DeviceInfo * Merge 'main' into 'sargon_os' * wip * Implement HostInfo struct (#189) * Implement HostInfo struct * Store host id in secure storage * Fix doc * Separate id and date from host info * Implement HostOS that will encapsulate the different values and constants of each host os * Expose uniffi method for creating device info from host info * wip * Sargon os v1 android (#196) * Implement sargon os drivers * Boot sargon os in example app * Remove active profile id * Add more unit tests * Fix apple tests * pr review * version bump --------- Co-authored-by: Alexander Cyon <[email protected]> Co-authored-by: micbakos-rdx <[email protected]> Co-authored-by: micbakos-rdx <[email protected]>
* Rust: `SargonOS` &`Drivers` more Profile Logic | Swift: Shared State (working example app)! (#131) Introducing SargonOS * MFA: Sec Structure of Factor <Source | ID | Instance>s (#150) MFA Batch 1: SecurityStructureOfFactorSources and MatrixOfFactorSoures and also on FactorSourceID and FactorInstance level. Also add some new FactorSourceKinds at least a POC version of them or stubbed out ones. * Sargon OS undo Profile JSON format change (#177) Revert Profile JSON format changes in DeviceInfo * Merge 'main' into 'sargon_os' * Implement HostInfo struct (#189) * Implement HostInfo struct * Store host id in secure storage * Fix doc * Separate id and date from host info * Implement HostOS that will encapsulate the different values and constants of each host os * Expose uniffi method for creating device info from host info * wip * Sargon os v1 android (#196) * Implement sargon os drivers * Boot sargon os in example app * Remove active profile id * Add more unit tests * Fix apple tests * Change profile holder to keep profile state instead of profile * Driver emits profile states instead of profile * Implement new and delete wallet functions * Remove unused comment * Implement import functionality * Propagate CRUD operations on profile to the profile state change driver. * Remove unnecessary comment * Fix swift tests * Fix doc * Prioritize bdfs save and then save profile in new wallet * Implement retry read mechanism for profile snapshot. This currently exists on Android codebase and needs revisiting. * Bump version --------- Co-authored-by: Alexander Cyon <[email protected]> Co-authored-by: Ghenadie Vasiliev-Pusca <[email protected]>
Note
Target branch is not
main
it issargon_os
branch...Abstract
Important
Read this description in full, it is more important than looking at the code. Since the PR is big it is much more important you read all of this text.
Introducing
SargonOS
- the backend for wallets, with Profile CRUD operations as well as persisting profile into secure storage and scaffolding for async stream of changes so that wallet can subscribe to an event stream of updates/changes and update the UI in a reactive manner.Through this document when I say "host client" I mean iOS or Android Radix wallet app. When I talk about
***Client
in the context of Rust code, I mean a type capable of doing something using a driver, e.g.SecureStorageClient
. I do not mean Swifts TCA style dependency when I write Client, always Rust (unless explicitly stated).Note
Tarpaulin incorrectly treats all
await
lines as "missed", I've raised a GH issue for this accuracy bug. So the PATCH coverage by this PR should in fact be close to 100%.Tip
You jump to a section using the Table-Of-Contents (ToC) directly and you can jump back to the ToC from the section header by pressing the
^
button next to each section header.Table Of Contents
Introduction ^
Tip
Please start by looking at this demo, a recording of the iOS example app in this repo, which is built using TCA and new "share state" features1.
In the video you will see me:
AccountsForDisplay
, and show account(s).trim.6F9F5BA4-F56F-4055-9159-87A401CAD87A.MOV
Note
I really recommend you checkout this branch, build it
./scripts/ios/build-sargon.sh
andopen examples/iOS/Planbok.xcworkspace
and play around a bit to understand it.Description ^
This is a not a production ready implementation of
SargonOS
- replacement of iOS/Kotlin walletsProfileStore
and clients - since many features are missing, e.g. everything related to Personas, AuthorizedDapps and LegacyAccount import to just name a few examples. However, the features which have been implemented are very near production ready. See Supported Features section below.If the analogy in this PR does not work well, we ought to change it, here it is:
Hosts
passDrivers
toBios
Bios
bootsSargonOS
SargonOS
loads existingProfile
or creates a newBDFS
andProfile
Hosts
commandsSargonOS
to perform actions e.g.createAndSaveNewAccount
SargonOS
usesDrivers
to perform the action and emitsEventNotification
s (using aDriver
)Hosts
have subscribed toEventNotification
s (using aDriver
) and react to the events (updating UI).SargonOS
^The
SargonOS
is the heart of the Sargon project and will be the heart of the host clients (iOS / Android wallet apps). There should always only be one instance, which you boot from a Bios (to which you have passedDrivers
). The SargonOS is#[uniffi::Object]
, a reference type and should host clients should use it to react to changes and command it to mutate profile, e.g. create and save accounts and SargonOS then persists the modified profile into secure storage, using theSecureStorageClient
(see Clients section below), which internally uses aSecureStorageDriver
passed to the SargonOS during boot from the BIOS.SargonOS
has a throwing asyncboot
constructor, in we try to load a saved profile from secure storage, which is async for flexibility reasons so that implementing clients can let it be async, which Android might need (@michaelrdx?).Important
If we realize that
SecureStorageDriver
for saving and loading ofProfile
specifically does not need to be async, we should change that and this might allow use to change many functions inSargonOS
to not be async. We I say "Profile
specifically" since we might need saving and loading ofMnemonics
to be async (on iOS at least), since those items requires Authentication to be read, thus must be async.SargonOS
is implemented like this:Which is an implementation I must say I'm quite happy with. The
Clients
type contain wrappers and convenience around each driver and most prominently haspub secure_storage: SecureStorageClient
.Clients implements
Defer, making it possible for us to call
self.secure_storage(where
selfis
&SargonOS), instead of having to write
self.clients.secure_storage`, greatly simplifying the code.The
ProfileHolder
is a mutable accessor around aProfile
, doing a bit of UniFFI interior mutability dance usingRwLock
, this allows us to access and modify profile without&mut SargonOS
- which does not work on aUniFFI
object.Supported Features
^This PR does not implement all features and logic relating to Profile which iOS/Android wallets do (then it would not be a 10k LOC PR but rather a 30k LOC one.... and would take several more weeks...), but it does, however, provide a decent amount of features for immediate use (or possibly with minor modifications). Here is a non-exhaustive / best effort attempt at enumeration of implemented features:
DeviceInfo
- a struct containing information about the device (upgraded with more fields).DeviceInfo
being intact)last_used_on_device
to the host clients device info)Furthermore, using the
EventBusDriver
, an instance of which host clients passes into theBIOS
upon bootingSargonOS
, host clients can listen/subscribe to an async stream ofEventNotifications
, such as:etc...
This have been used with great success in the iOS Example App to build TCA Shared State, with which I've built UI which automagically updates without any listener setup boilerplate needed at all.
So as you understand,
SargonOS
"emits" thoseEventNotifcation
s for each mutating method respectively.Bios
^The BIOS is a small struct looking like this:
Having one single purpose, which is to initialize subsystems (
install_logger
), for more information see Subsystems section below, otherwise essentially a wrapper aroundDrivers
collection.Drivers
^A
Driver
is a trait, which is UniFFI exported, meaning it will be a Swift Protocol / Kotlin Interface, which host clients can conform to. Host clients pass instances of their conforming types to theBIOS
which is passed to theSargonOS
upon booting.Drivers
is a simple type holding onto eachDriver
, that the host clients need to conform to and initialize, it looks like this:Rust uses the
Driver
s to implement higher level abstractions calledClients
, to perform specific tasks, e.g. File I/O or CRUD operations on secure (Swift: "Keychain") or unsecure storage (Swift: "UserDefaults").Note
UniFFI has a limitation on the traits, all Swift Protocols are marked
protocol Fooable: AnyObject
, meaning we must use reference types to implement these.Quite recently (a couple of months back) UniFFI added support for async trait methods, which makes it very easy to build async functionality such as File I/O and Networking!
Warning
Due to how UniFFI works, we are UNABLE to get the Driver class instance from SargonOS once booted. See explanation below:
eb
of (swift)class EventBus
(conforming toEventBusDriver
)eb
it to biosb
and bootSargonOS
with said biosb
SargonOS
would#[uniffi::export]
a method that returns theEventBus
driver,eb
!EventBus
, instead it will be some UniFFI generatedEventDriverImpl
class.The implications of this is that the EventBus instance SHOULD be a
static let shared
in Swift (and similar in Kotlin) so that we can subscribe to the events.If UniFFI would have supported some kind of
AsyncSequence
on the Rust side (no such type exist natively in Rust, but maybe doable with a dependency, maybe.), we could have exported that async sequence to the FFI side. But since not doable, we SHOULD build that async sequence in our implementing Driver. So this is what I've done, and it works well. See the coming to sections for details.Driver Rust Side
^Here is the
EventBusDriver
on the Rust side:SargonOS
will then install an instance of this driver (Swift / Kotlin provided instance), into a (currently trivial type)EventBusClient
and use that client to emit relevant event notification, looking like this:Where
Event
is a nested enum with some small(!!) associated values:From an event we can query its discriminant, of type
EventKind
, which has no associated values, useful for filtering on EventNotifications on the FFI side.Here is the implementation of
save_profile
inSargonOS
:After having successfully persisted
profile
usingsecure_storage.save
the OS emitsEvent::ProfileSaved
on theevent_bus
resulting in anEventNotifaction
the host clients will receive and once handled, the functionsave_profile
on the Rust side will return.Driver FFI Side
^Here is the
EventBusDriver
Swift implementation:When booting the
SargonOS
with the BIOS we will useEventBus.shared
asEventBusDriver
, now used by Rust and by iOS App (using Swift Sargon).Clients
^Client
s are higher level abstractions built on top of drivers, translating some needed-to-be-UniFFI-exported types into Rust types. Here isHostInfoClient
:All clients have the same "shape" in the sense that they have a single field, called
driver
beingArc<dyn HostInfoDriver>
(which is akinany HostInfoDriver
in Swift), and a constructor callednew
.The
HostInfoClient
has a method:Which using the driver constructs a
DeviceInfo
instance, used for fresh-app-install users to create theDeviceInfo
, representing their host device (phone).The
EventBusClient
"invert" the notifier-notifiee relationship, because from aSargonOS
perspective it makes sense to tell itsevent_client
to "emit" a notification, rather than asking the host client to "handle a notification".Subsystems
^Some drivers might not map into a client at all, because they might need "their own life time". Those drivers are use to
init
"sub systems". At the time of writing only one such driver / "sub system" pair exists -LoggingDriver
andLogSystem
. TheLogginDriver
is used by host clients to be able to receive "forwarding of log calls" from Rust into FFI, while still being able to use Rust's neatinfo!
anddebug!
andwarn!
macros. This makes it possible to see logs from rust, inside of Xcode with pretty colors and metadata, without loosing any convenience Rust side, we can use the de facto industry standardlog
crate with the above mentioned logging macros. For more details about how to use logging see the Logging section.The
LoggingDriver
looks like this:And the
LogSystem
looks like this:We impl
log::Log
forLogSystem
, so that an instance ofLogSystem
can be used as Rust logger, and this instance will calllog
on thedriver
, forwarding the logging to the host client (FFI side).Declare
LOG
as aLogSystem
"singleton", starting without any value, and will be set once "initialized".Declare private init method - for those new to Rust, a
fn
is always private if not annotatedpub
.Declare public - visible for Rust side -
install_logger
method which takes alogging_driver
and installs it into theLOG
singleton log "sub system", callinginit
which hooks thisLOG
to thelog
crates macrosdebug!
,warn!
,info!
etc...Finally ensure to call
install_logger
when creating the bios (done before theSargonOS
is booted)!.We also UniFFI export
install_logger
useful for unit tests in Swift / KotlinChanges ^
Note
This is only a best effort summary of the most important changes,
LOC ^
Using
cloc
to count lines of code changes:Profile Format ^
This PR changes Profile Format - still backwards compatible as always, meaning old Profiles will be autoupgraded into the new format, but Profiles created with this version of Sargon is not compatible with old wallets (as always).
The following change has been made into
DeviceInfo
:The following changes have been made into
DeviceFactorSourceHint
:This is to provide more information about the Host Device creating and using the Profile and also show more information about a Mnemonic.
Rust ^
Wallet
, replaced by...ProfileHolder
, which just holds a mutable reference to the Profile (RwLock
).SargonOS
which as an UF exported async throwing constructorasync fn boot(bios: Bios) -> Result<()>
Bios
, which holds onto a collection ofDrivers
, which host client declare and pass toBios
before boot.Drivers
collection, holding onto eachDriver
trait that walletDriver
in a Client, besidesLoggingDeriver
which is used by the `LogSystemClients
collection, holding onto eachClient
.Swift ^
For iOS Example app see below, this section describes changes in Swift Sargon.
Drivers
, they might not be fully production ready, especially forSecureStorageClient
(Keychain), we need to make a decision if it is something we want to put in Swift Sargon or if it should be in the iOS wallet (iOS Example App does however impl an POC variant).SargonOS
andBIOS
, especially declaringstatic var shared
allowing us to have singletons for those but please note those are ASYNC set so might be nil and cause crash if access before SargonOS has booted. As long as our app does not try to eagerly access the the object we are fine.TestOS
class which is effectively a builder, wrappingSargonOS
, where each (async throwing) method returnsSelf
(wrapper aroundSargonOS
), so that we can chain method calls, like soos.createAccount().createAccount()
creating two accounts (which is a silly example since you can alsoos.batchCreateAccounts(count: 2, namePrefix: "Unnamed")
).Kotlin ^
I have not implemented any
Driver
s other than updating theNetworkAntenna
->NetworkingDriver
to make the Kotlin project compile and tests pass.Future Work ^
Once this PR gets merged after having applied useful feedback, Android devs ought to implement the Drivers in Kotlin and perhaps update the Android demo app to evaluate "how it feels" (typically took me 2 days).
Important
It is important Android team familize themselves with the iOS example app and the
SargonOS
and evaluate if the "API" works. We need this feedback and make adjustment BEFORE iOS team starts migrating over toSargonOS
.After the Android team has given 👍🏽 on SargonOS, or made adjustment, then iOS team should be able to start migration in Radix iOS Wallet to use
SargonOS
and re-implement (harden, make final production ready) my near-production-ready Shared State (@SharedReader
) implementations. The iOS Example App is so mature enought to act as inspiration. If implementation in that example app is not GREAT, then make adjustments - synced with Android team - and make it GREAT, then copy paste over the same solution to the iOS app. I think it is already pretty great, so we might be able to do it directly.SargonOS
should already be almost a 1:1 replacement ofProfileStore
andSecureStorageClient
on iOS.Btw, we really should move out all logic on the Rust side from the
v100
module (folder) containing the models ofProfile
, into the "sibling" folderlogic
. So thatv100
only contains models, and all methods and functions be placed inlogic
, split into perhaps separate modules (files) with good name, likeprofile::logic::account::create_account
andprofile::logic::account::query_accounts
.Gradual adoption ^
Thanks to the
save_profile
UniFFI exported method, it is possible, albeit, discouraged to changes to profile on the FFI side and tell SargonOS to save it. In the land of Swift it would look like this:Where the "cost"
1R
means roundtripping the WHOLE profile accross the UniFFI boundary, which is an expensive operation (takes time), and total cost is actually3R
, since when we save the profile we send it back to UniFFI accross the UniFFI boundary, and thensecure_storage
will save it, which ones again sends it back from UniFFI to FFI as data. But this is acually not true, the actual cost is much much greater, because how Kotlin and Swift will want to react to events in the EventBusDriver impl... Let me explain by looking atchange_current_gateway
:Apart from the big win of having logic for changing current gateway in one place, we also see that we emit a notification if the gateway changed. In Host clients we can use the value
to
emitted directly and update UI based on that, no more UniFFI roundtrop needed, especially can we AVOID theS * R
, whereS
denotes the number of subscriptions to the change of gateway event, andR
once again denotes the cost of roundtripping the whole of Profile.Lets now say we defer the migration of saving a
Persona
into Profile, so we use the "loop hole":Then if we wanna write a subscriber for
personas
we cannot listen for apersonaAdded
event using our EventBus because no such event exists!. Instead we can listen to the eventprofileSaved
, but how do we read out the personas? Well we MUST read the whole of Profile, i.e. a cost ofS * R
(1R
per subscriber). But I've actually POCed that it is possible to have one single subscriber per type, i.e. can be one singlePersonas
shared state subscriber, so only costing1R
forPersonas
.Note
But by adding just a few lines of code in Rust, the
save_persona_to_profile
method and a newEventKind
addedPersona
and a simplepub fn personas() -> Personas
which would take me cirka 20 minutes with test, we suddenly remove the(3 + S) * P
cost! And we get a constant tiny cost.But nevertheless, the
saveProfile
method allows for gradual adoption! We should aim to migrate a few methods from Swift/Kotlin into Rust per week, then after a few months we will have it all inside of Rust Sargon! 🎉Logging ^
Logging is such a crucial part of development and debugging that I wanted to dedicate an entire section about it, because logging in SargonOS / Sargon and in host clients is amazing to use thanks to this PR.
As you can see in the screenshot above, Xcode treats those logged messages really well, and we can chose which metadata we wanna include, timestamp, category (Rust or Swift), and we can filter on those metadata too! All messages logged from Rust show up with
Rust
and those logged from inside of Swift show up withSwift
.Swift Sargon SHOULD make use of the public global logger named
log
- which the iOS wallet also can start using - instead ofloggerGlobal
.iOS Example App ^
The iOS app example app has been upgraded to use TCA Shared State, demonstrating example usage of the
EventBusDriver
.Note
The ONLY thing you need to do to get access to autoupdating list of accounts (non hidden, on current network) is declare
@SharedReader(.accountsForDisplay) var accountsForDisplay
in TCA State. Yes, that is it 🎉This is the AccountsFeature's
State
:An usage in the view:
And the
SharedReader
is very easy to implement:Footnotes
Shared STate is a way to extremely easily listen to async streams of updates in UI, recently (May 2024) introduced in TCA 1.10.0 ↩