Skip to content
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

[ABW-1316] Split out signing and submission from TransactionClient, introduce Signing feature (prepares for signing w Ledger) #455

Merged
merged 58 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
2a3bb34
WIP
CyonAlexRDX Apr 21, 2023
28a9e5d
WIP
CyonAlexRDX Apr 21, 2023
ae9c312
WIP
CyonAlexRDX Apr 21, 2023
8737fd6
WIP
CyonAlexRDX Apr 21, 2023
4892605
WIP
CyonAlexRDX Apr 24, 2023
4dbb60b
WIP
CyonAlexRDX Apr 24, 2023
948e7d8
WIP
CyonAlexRDX Apr 24, 2023
4cd1ff9
WIP
CyonAlexRDX Apr 24, 2023
07834aa
WIP
CyonAlexRDX Apr 24, 2023
d4b61be
WIP
CyonAlexRDX Apr 24, 2023
56353cf
WIP
CyonAlexRDX Apr 24, 2023
d59eba6
WIP
CyonAlexRDX Apr 24, 2023
4551af7
WIP
CyonAlexRDX Apr 24, 2023
9e3cfe0
WIP
CyonAlexRDX Apr 24, 2023
2feb96e
WIP
CyonAlexRDX Apr 24, 2023
6d2344f
WIP
CyonAlexRDX Apr 25, 2023
308453b
Merge branch 'main' into ABW-1316_sign_tx_with_ledger
CyonAlexRDX Apr 25, 2023
1b43e93
WIP
CyonAlexRDX Apr 25, 2023
3e7e3ed
merge
CyonAlexRDX Apr 25, 2023
951cfb9
WIP
CyonAlexRDX Apr 25, 2023
540030a
WIP
CyonAlexRDX Apr 25, 2023
97f2689
WIP
CyonAlexRDX Apr 25, 2023
892a5e0
WIP
CyonAlexRDX Apr 25, 2023
13907d7
WIP
CyonAlexRDX Apr 26, 2023
9c995ec
merge
CyonAlexRDX Apr 26, 2023
faa450e
WIP
CyonAlexRDX Apr 26, 2023
d944cb9
WIP
CyonAlexRDX Apr 26, 2023
112bb0f
WIP
CyonAlexRDX Apr 26, 2023
67454a4
WIP
CyonAlexRDX Apr 26, 2023
469e3e5
WIP
CyonAlexRDX Apr 26, 2023
09f05f1
WIP
CyonAlexRDX Apr 27, 2023
480eaf2
WIP
CyonAlexRDX Apr 27, 2023
02e12b9
WIP
CyonAlexRDX Apr 27, 2023
138e1c2
WIP
CyonAlexRDX Apr 27, 2023
449d0a0
WIP
CyonAlexRDX Apr 27, 2023
b260dfc
WIP
CyonAlexRDX Apr 27, 2023
aa854f2
WIP
CyonAlexRDX Apr 27, 2023
063fc3c
WIP
CyonAlexRDX Apr 27, 2023
3c1f876
WIP
CyonAlexRDX Apr 27, 2023
2dd6225
WIP
CyonAlexRDX Apr 27, 2023
55d2baa
WIP
CyonAlexRDX Apr 27, 2023
e67796c
WIP
CyonAlexRDX Apr 27, 2023
aa23fe7
WIP
CyonAlexRDX Apr 27, 2023
3024330
WIP
CyonAlexRDX Apr 27, 2023
4bf6df8
WIP
CyonAlexRDX Apr 27, 2023
13ed5b3
WIP
CyonAlexRDX Apr 27, 2023
c515628
WIP
CyonAlexRDX Apr 27, 2023
fc7eb2d
WIP
CyonAlexRDX Apr 27, 2023
160969b
WIP
CyonAlexRDX Apr 27, 2023
32c627c
WIP
CyonAlexRDX Apr 27, 2023
57d2f6d
WIP
CyonAlexRDX Apr 27, 2023
efba649
WIP
CyonAlexRDX Apr 27, 2023
20550c8
WIP
CyonAlexRDX Apr 27, 2023
653e021
WIP
CyonAlexRDX Apr 27, 2023
c8f9d38
WIP
CyonAlexRDX Apr 28, 2023
10f5b33
WIP
CyonAlexRDX Apr 28, 2023
21aac12
WIP
CyonAlexRDX Apr 28, 2023
68e2069
WIP
CyonAlexRDX Apr 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ package.addModules([
.feature(
name: "AssetTransferFeature",
dependencies: [
"TransactionSigningFeature",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
],
tests: .yes()
),
Expand Down Expand Up @@ -249,6 +249,17 @@ package.addModules([
],
tests: .yes()
),
.feature(
name: "SigningFeature",
featureSuffixDroppedFromFolderName: true,
dependencies: [
"TransactionClient",
"FactorSourcesClient",
"Profile",
"UseFactorSourceClient",
],
tests: .no
),
.feature(
name: "SplashFeature",
dependencies: [
Expand All @@ -262,19 +273,11 @@ package.addModules([
dependencies: [
"GatewayAPI",
"TransactionClient",
"SigningFeature",
"SubmitTransactionClient",
],
tests: .yes()
),
.feature(
name: "TransactionSigningFeature",
dependencies: [
"GatewayAPI",
"GatewaysClient",
"TransactionClient",
],
tests: .yes()
),

])

// MARK: - Clients
Expand Down Expand Up @@ -374,6 +377,7 @@ package.addModules([
dependencies: [
"FactorSourcesClient",
"ProfileStore",
"AccountsClient",
],
tests: .yes()
),
Expand All @@ -384,6 +388,7 @@ package.addModules([
"EngineToolkitClient",
"GatewayAPI",
"GatewaysClient", // getCurrentNetworkID
"SubmitTransactionClient",
"TransactionClient",
],
tests: .yes()
Expand Down Expand Up @@ -555,18 +560,25 @@ package.addModules([
],
tests: .yes()
),
.client(
name: "SubmitTransactionClient",
dependencies: [
"EngineToolkitClient",
"GatewayAPI",
],
tests: .no
),
.client(
name: "TransactionClient",
dependencies: [
"AccountsClient",
"AccountPortfoliosClient",
"CacheClient",
"EngineToolkitClient",
"FactorSourcesClient",
"GatewayAPI",
"GatewaysClient",
"SecureStorageClient",
"EngineToolkitClient",
"UseFactorSourceClient",
"GatewayAPI",
],
tests: .yes()
),
Expand All @@ -575,6 +587,7 @@ package.addModules([
dependencies: [
"Profile",
"Cryptography",
"FactorSourcesClient",
"SecureStorageClient",
],
tests: .no
Expand Down
11 changes: 11 additions & 0 deletions Sources/Clients/CacheClient/CacheClient+Interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,14 @@ extension CacheClient {
case entryLifetimeExpired
}
}

extension CacheClient {
@Sendable
public func clearCacheForAccounts(_ accounts: Set<AccountAddress>) {
GhenadieVP marked this conversation as resolved.
Show resolved Hide resolved
if !accounts.isEmpty {
accounts.forEach { self.removeFile(.accountPortfolio(.single($0.address))) }
} else {
self.removeFolder(.accountPortfolio(.all))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct EngineToolkitClient: Sendable, DependencyKey {
public var compileTransactionIntent: CompileTransactionIntent
public var compileSignedTransactionIntent: CompileSignedTransactionIntent
public var compileNotarizedTransactionIntent: CompileNotarizedTransactionIntent
public var decompileTransactionIntentRequest: DecompileTransactionIntentRequest

public var deriveOlympiaAdressFromPublicKey: DeriveOlympiaAdressFromPublicKey

Expand Down Expand Up @@ -68,6 +69,8 @@ extension EngineToolkitClient {
public typealias GenerateTransactionReview = @Sendable (AnalyzeManifestWithPreviewContextRequest) throws -> AnalyzeManifestWithPreviewContextResponse

public typealias DecodeAddressRequest = @Sendable (String) throws -> DecodeAddressResponse

public typealias DecompileTransactionIntentRequest = @Sendable (EngineToolkitModels.DecompileTransactionIntentRequest) throws -> DecompileTransactionIntentResponse
}

// MARK: - AccountAddressesInvolvedInTransactionRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ extension EngineToolkitClient {
compileNotarizedTransactionIntent: {
try engineToolkit.compileNotarizedTransactionIntentRequest(request: $0).get()
},
decompileTransactionIntentRequest: {
try engineToolkit.decompileTransactionIntentRequest(request: $0).get()
},
deriveOlympiaAdressFromPublicKey: {
try engineToolkit.deriveOlympiaAddressFromPublicKeyRequest(
request: .init(network: .mainnet, publicKey: $0.intoEngine())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ extension EngineToolkitClient: TestDependencyKey {
compileTransactionIntent: { _ in .init(compiledIntent: [0xDE, 0xAD]) },
compileSignedTransactionIntent: { _ in .init(bytes: [0xDE, 0xAD]) },
compileNotarizedTransactionIntent: { _ in .init(compiledIntent: [0xDE, 0xAD]) },
decompileTransactionIntentRequest: { _ in throw NoopError() },
deriveOlympiaAdressFromPublicKey: { _ in throw NoopError() },
generateTXID: { _ in "deadbeef" },
accountAddressesNeedingToSignTransaction: { _ in [] },
Expand All @@ -31,6 +32,7 @@ extension EngineToolkitClient: TestDependencyKey {
compileTransactionIntent: unimplemented("\(Self.self).compileTransactionIntent"),
compileSignedTransactionIntent: unimplemented("\(Self.self).compileSignedTransactionIntent"),
compileNotarizedTransactionIntent: unimplemented("\(Self.self).compileNotarizedTransactionIntent"),
decompileTransactionIntentRequest: unimplemented("\(Self.self).decompileTransactionIntentRequest"),
deriveOlympiaAdressFromPublicKey: unimplemented("\(Self.self).deriveOlympiaAdressFromPublicKey"),
generateTXID: unimplemented("\(Self.self).generateTXID"),
accountAddressesNeedingToSignTransaction: unimplemented("\(Self.self).accountAddressesNeedingToSignTransaction"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ public struct FactorSourcesClient: Sendable {
public var addPrivateHDFactorSource: AddPrivateHDFactorSource
public var checkIfHasOlympiaFactorSourceForAccounts: CheckIfHasOlympiaFactorSourceForAccounts
public var addOffDeviceFactorSource: AddOffDeviceFactorSource
public var getSigningFactors: GetSigningFactors

public init(
getFactorSources: @escaping GetFactorSources,
factorSourcesAsyncSequence: @escaping FactorSourcesAsyncSequence,
addPrivateHDFactorSource: @escaping AddPrivateHDFactorSource,
checkIfHasOlympiaFactorSourceForAccounts: @escaping CheckIfHasOlympiaFactorSourceForAccounts,
addOffDeviceFactorSource: @escaping AddOffDeviceFactorSource
addOffDeviceFactorSource: @escaping AddOffDeviceFactorSource,
getSigningFactors: @escaping GetSigningFactors
) {
self.getFactorSources = getFactorSources
self.factorSourcesAsyncSequence = factorSourcesAsyncSequence
self.addPrivateHDFactorSource = addPrivateHDFactorSource
self.checkIfHasOlympiaFactorSourceForAccounts = checkIfHasOlympiaFactorSourceForAccounts
self.addOffDeviceFactorSource = addOffDeviceFactorSource
self.getSigningFactors = getSigningFactors
}
}

Expand All @@ -31,6 +34,28 @@ extension FactorSourcesClient {
public typealias AddPrivateHDFactorSource = @Sendable (PrivateHDFactorSource) async throws -> FactorSourceID
public typealias CheckIfHasOlympiaFactorSourceForAccounts = @Sendable (NonEmpty<OrderedSet<OlympiaAccountToMigrate>>) async -> FactorSourceID?
public typealias AddOffDeviceFactorSource = @Sendable (FactorSource) async throws -> Void
public typealias GetSigningFactors = @Sendable (NetworkID, NonEmpty<Set<Profile.Network.Account>>) async throws -> SigningFactors
}

public typealias SigningFactors = NonEmpty<OrderedSet<SigningFactor>>

// MARK: - SigningFactor
public struct SigningFactor: Sendable, Hashable {
public let factorSource: FactorSource
public let signers: NonEmpty<Set<Signer>>
public init(factorSource: FactorSource, signers: NonEmpty<Set<Signer>>) {
self.factorSource = factorSource
self.signers = signers
}

public struct Signer: Sendable, Hashable {
public let account: Profile.Network.Account
public let factorInstancesRequiredToSign: Set<FactorInstance>
public init(account: Profile.Network.Account, factorInstancesRequiredToSign: Set<FactorInstance>) {
self.account = account
self.factorInstancesRequiredToSign = factorInstancesRequiredToSign
}
}
}

extension FactorSourcesClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ extension FactorSourcesClient: TestDependencyKey {
factorSourcesAsyncSequence: unimplemented("\(Self.self).factorSourcesAsyncSequence"),
addPrivateHDFactorSource: unimplemented("\(Self.self).addPrivateHDFactorSource"),
checkIfHasOlympiaFactorSourceForAccounts: unimplemented("\(Self.self).checkIfHasOlympiaFactorSourceForAccounts"),
addOffDeviceFactorSource: unimplemented("\(Self.self).addOffDeviceFactorSource")
addOffDeviceFactorSource: unimplemented("\(Self.self).addOffDeviceFactorSource"),
getSigningFactors: unimplemented("\(Self.self).getSigningFactors")
)

public static let noop = Self(
getFactorSources: { throw NoopError() },
factorSourcesAsyncSequence: { AsyncLazySequence([]).eraseToAnyAsyncSequence() },
addPrivateHDFactorSource: { _ in throw NoopError() },
checkIfHasOlympiaFactorSourceForAccounts: { _ in nil },
addOffDeviceFactorSource: { _ in }
addOffDeviceFactorSource: { _ in },
getSigningFactors: { _, _ in throw NoopError() }
)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AccountsClient
import ClientPrelude
import FactorSourcesClient
import ProfileStore
Expand Down Expand Up @@ -72,12 +73,98 @@ extension FactorSourcesClient: DependencyKey {
return nil // failure
}
},
addOffDeviceFactorSource: addOffDeviceFactorSource
addOffDeviceFactorSource: addOffDeviceFactorSource,
getSigningFactors: { _, accounts in
@Dependency(\.accountsClient) var accountsClient

let allFactorSources = try await getFactorSources()

final class SigningFactorRef {
let factorSource: FactorSource
var signers: [Profile.Network.Account.ID: SignerRef] = [:]
init(factorSource: FactorSource, signers: [Profile.Network.Account.ID: SignerRef] = [:]) {
self.factorSource = factorSource
self.signers = signers
}

final class SignerRef {
let account: Profile.Network.Account
var factorInstancesRequiredToSign: Set<FactorInstance>
init(account: Profile.Network.Account, factorInstancesRequiredToSign: Set<FactorInstance> = .init()) {
self.account = account
self.factorInstancesRequiredToSign = factorInstancesRequiredToSign
}

func valueType() -> SigningFactor.Signer {
.init(account: account, factorInstancesRequiredToSign: factorInstancesRequiredToSign)
}
}

func valueType() throws -> SigningFactor {
guard let signersNonEmpty = NonEmpty<Set<SigningFactor.Signer>>(rawValue: Set(self.signers.values.map { $0.valueType() })) else {
throw SignersUnexpectedlyEmpty()
}
return .init(factorSource: factorSource, signers: signersNonEmpty)
}
}

var signingFactors: [FactorSourceID: SigningFactorRef] = [:]

for account in accounts {
switch account.securityState {
case let .unsecured(unsecuredEntityControl):
let factorInstance = unsecuredEntityControl.genesisFactorInstance
let id = factorInstance.factorSourceID
guard let factorSource = allFactorSources[id: id] else {
assertionFailure("Bad! factor source not found")
throw FactorSourceNotFound()
}
let outerRef = signingFactors[id, default: SigningFactorRef(factorSource: factorSource)]
let innerRef = outerRef.signers[account.id, default: .init(account: account)]
innerRef.factorInstancesRequiredToSign.insert(factorInstance)
outerRef.signers[account.id] = innerRef
signingFactors[id] = outerRef
}
}

guard let signersNonEmpty = try SigningFactors(
rawValue: OrderedSet(uncheckedUniqueElements: signingFactors.values.map { try $0.valueType() }.sorted())
) else {
throw FailedToFindSigners()
}
return signersNonEmpty
}
)
}

public static let liveValue = Self.live()
}

// MARK: - SignersUnexpectedlyEmpty
struct SignersUnexpectedlyEmpty: Error {}

// MARK: - FailedToFindSigners
struct FailedToFindSigners: Error {}

// MARK: - FactorSourceAlreadyPresent
struct FactorSourceAlreadyPresent: Swift.Error {}

// MARK: - FactorSourceNotFound
struct FactorSourceNotFound: Swift.Error {}

// MARK: - SigningFactor + Comparable
extension SigningFactor: Comparable {
public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.factorSource.kind.signingOrder < rhs.factorSource.kind.signingOrder
}
}

extension FactorSourceKind {
fileprivate var signingOrder: Int {
switch self {
case .ledgerHQHardwareWallet: return 0
case .device: return 1
default: return 1000
}
}
}
35 changes: 28 additions & 7 deletions Sources/Clients/FaucetClient/FaucetClient+Live.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import ClientPrelude
import Cryptography
import EngineToolkitClient
import EngineToolkitModels
import GatewayAPI
import GatewaysClient
import SubmitTransactionClient
import TransactionClient

let minimumNumberOfEpochsPassedForFaucetToBeReused = 1
Expand Down Expand Up @@ -41,18 +44,36 @@ extension FaucetClient: DependencyKey {
await isAllowedToUseFaucetIfSoGetEpochs(accountAddress: accountAddress) != nil
}

/// This function can ONLY sign transaction which does not spend funds or otherwise require
GhenadieVP marked this conversation as resolved.
Show resolved Hide resolved
/// auth, since we use an ephemeral key pair
@Sendable func signSubmitTX(manifest: TransactionManifest) async throws {
@Dependency(\.transactionClient) var transactionClient
@Dependency(\.engineToolkitClient) var engineToolkitClient
@Dependency(\.submitTXClient) var submitTXClient

let signSubmitTXRequest = SignManifestRequest(
manifestToSign: manifest,
makeTransactionHeaderInput: .default
let networkID = await gatewaysClient.getCurrentNetworkID()

let ephemeralNotary = Curve25519.Signing.PrivateKey()
GhenadieVP marked this conversation as resolved.
Show resolved Hide resolved

let builtTransactionIntentWithSigners = try await transactionClient.buildTransactionIntent(
.init(
networkID: networkID,
manifest: manifest,
selectNotary: { _ in
.init(notary: .ephemeralPublicKey(.eddsaEd25519(ephemeralNotary.publicKey)), notaryAsSignatory: true)
}
)
)

let _ = try await transactionClient
.signAndSubmitTransaction(signSubmitTXRequest)
.asyncFlatMap(transform: transactionClient.getTransactionResult)
.get()
let transactionIntent = builtTransactionIntentWithSigners.intent
let compiledIntent = try engineToolkitClient.compileTransactionIntent(transactionIntent)

let notarized = try await transactionClient.notarizeTransaction(.init(intentSignatures: [], compileTransactionIntent: compiledIntent, notary: .curve25519(ephemeralNotary)))
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved

let txID = notarized.txID
_ = try await submitTXClient.submitTransaction(.init(txID: txID, compiledNotarizedTXIntent: notarized.notarized))

try await submitTXClient.hasTXBeenCommittedSuccessfully(txID)
}

let getFreeXRD: GetFreeXRD = { faucetRequest in
Expand Down
Loading