Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 11 additions & 11 deletions Code.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
88FB56E62EE0AB810014EC5F /* 2025-11-27-swap-rpc-analysis.md in Resources */ = {isa = PBXBuildFile; fileRef = 88FB56E12EE0AB810014EC5F /* 2025-11-27-swap-rpc-analysis.md */; };
88FB56E82EE0C3150014EC5F /* 2025-12-03-swap-models-startswap.md in Resources */ = {isa = PBXBuildFile; fileRef = 88FB56E72EE0C3150014EC5F /* 2025-12-03-swap-models-startswap.md */; };
9A017D692EAC1DE200216925 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 9A017D682EAC1DE200216925 /* Kingfisher */; };
9A2396762E7DE27500D70699 /* SolanaSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9A2396752E7DE27500D70699 /* SolanaSwift */; };
508AF9C6D67C4CD29DE10A16 /* TweetNacl in Frameworks */ = {isa = PBXBuildFile; productRef = 2B166ABF94584FEF9C368C81 /* TweetNacl */; };
9A444EE72E8C414D002B1E39 /* BigDecimal in Frameworks */ = {isa = PBXBuildFile; productRef = 9A444EE62E8C414D002B1E39 /* BigDecimal */; };
9A780F972EDF817D009BC4D9 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 9A780F962EDF817D009BC4D9 /* CodeScanner */; };
9ABDD1952D9D7B61006B6CDA /* FlipcashCore in Frameworks */ = {isa = PBXBuildFile; productRef = 9ABDD1942D9D7B61006B6CDA /* FlipcashCore */; };
Expand Down Expand Up @@ -96,7 +96,7 @@
9A017D692EAC1DE200216925 /* Kingfisher in Frameworks */,
9A444EE72E8C414D002B1E39 /* BigDecimal in Frameworks */,
9AC011182DA4320F0030298E /* FlipcashUI in Frameworks */,
9A2396762E7DE27500D70699 /* SolanaSwift in Frameworks */,
508AF9C6D67C4CD29DE10A16 /* TweetNacl in Frameworks */,
9AC0156C2DA576FA0030298E /* opencv2.framework in Frameworks */,
9ABDD1952D9D7B61006B6CDA /* FlipcashCore in Frameworks */,
9ADEF1D92DD627C6001B260A /* Mixpanel in Frameworks */,
Expand Down Expand Up @@ -217,7 +217,7 @@
9AD1265D2DA98ADC0048141F /* SQLite */,
9ADEF1D62DD627C0001B260A /* Bugsnag */,
9ADEF1D82DD627C6001B260A /* Mixpanel */,
9A2396752E7DE27500D70699 /* SolanaSwift */,
2B166ABF94584FEF9C368C81 /* TweetNacl */,
9A444EE62E8C414D002B1E39 /* BigDecimal */,
9A017D682EAC1DE200216925 /* Kingfisher */,
9A780F962EDF817D009BC4D9 /* CodeScanner */,
Expand Down Expand Up @@ -346,7 +346,7 @@
9A476D482978884700C5B5CE /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */,
9A9499D62CFB829300DC219B /* XCRemoteSwiftPackageReference "SQLite" */,
9A8D9A3D2D78C2E200E755B9 /* XCRemoteSwiftPackageReference "Kingfisher" */,
9A2396742E7DE27500D70699 /* XCRemoteSwiftPackageReference "solana-swift" */,
10A6C0E8F8974C1887219D41 /* XCRemoteSwiftPackageReference "tweetnacl-swiftwrap" */,
9A444EE52E8C414D002B1E39 /* XCRemoteSwiftPackageReference "BigDecimal" */,
9A780F952EDF817D009BC4D9 /* XCLocalSwiftPackageReference "CodeScanner" */,
);
Expand Down Expand Up @@ -1177,12 +1177,12 @@
minimumVersion = 10.24.0;
};
};
9A2396742E7DE27500D70699 /* XCRemoteSwiftPackageReference "solana-swift" */ = {
10A6C0E8F8974C1887219D41 /* XCRemoteSwiftPackageReference "tweetnacl-swiftwrap" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/dbart01/solana-swift";
repositoryURL = "https://github.com/bitmark-inc/tweetnacl-swiftwrap";
requirement = {
branch = main;
kind = branch;
kind = upToNextMajorVersion;
minimumVersion = 1.0.2;
};
};
9A444EE52E8C414D002B1E39 /* XCRemoteSwiftPackageReference "BigDecimal" */ = {
Expand Down Expand Up @@ -1243,10 +1243,10 @@
package = 9A8D9A3D2D78C2E200E755B9 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
9A2396752E7DE27500D70699 /* SolanaSwift */ = {
2B166ABF94584FEF9C368C81 /* TweetNacl */ = {
isa = XCSwiftPackageProductDependency;
package = 9A2396742E7DE27500D70699 /* XCRemoteSwiftPackageReference "solana-swift" */;
productName = SolanaSwift;
package = 10A6C0E8F8974C1887219D41 /* XCRemoteSwiftPackageReference "tweetnacl-swiftwrap" */;
productName = TweetNacl;
};
9A444EE62E8C414D002B1E39 /* BigDecimal */ = {
isa = XCSwiftPackageProductDependency;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 18 additions & 50 deletions Flipcash/Core/Controllers/Deep Links/Wallet/WalletConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import UIKit
import FlipcashUI
import FlipcashCore
import TweetNacl
import SolanaSwift

private let logger = Logger(label: "flipcash.wallet-connection")

Expand Down Expand Up @@ -75,7 +74,7 @@ public final class WalletConnection {
private let owner: AccountCluster
private let client: Client

private let rpc: WalletRPC
private let rpc: any SolanaRPC

/// Pending swap info to use when Phantom returns with signed transaction
private var pendingSwap: PendingSwap?
Expand All @@ -102,15 +101,10 @@ public final class WalletConnection {

// MARK: - Init -

init(owner: AccountCluster, client: Client, rpc: WalletRPC? = nil) {
init(owner: AccountCluster, client: Client, rpc: any SolanaRPC = SolanaJSONRPCClient()) {
self.owner = owner
self.client = client
self.rpc = rpc ?? JSONRPCAPIClient(
endpoint: .init(
address: "https://api.mainnet-beta.solana.com",
network: .mainnetBeta
)
)
self.rpc = rpc

if let connectedWalletSession = Keychain.connectedWalletSession {
self.session = connectedWalletSession
Expand Down Expand Up @@ -357,10 +351,10 @@ public final class WalletConnection {
// and flip `isFailed` so the processing screen surfaces the error.
do {
let signature = try await rpc.sendTransaction(
transaction: txBase64,
configs: .init(encoding: "base64")!
txBase64,
configuration: SolanaSendTransactionConfig()
)
logger.info("Transaction sent", metadata: ["signature": "\(signature)"])
logger.info("Transaction sent", metadata: ["signature": "\(signature.base58)"])
Analytics.track(event: Analytics.WalletEvent.transactionsSubmitted)
} catch {
logger.error("Chain submission failed", metadata: ["error": "\(error)"])
Expand All @@ -384,22 +378,22 @@ public final class WalletConnection {
) async -> SimulationOutcome {
do {
_ = try await rpc.simulateTransaction(
transaction: txBase64,
configs: RequestConfiguration(
commitment: "confirmed",
encoding: "base64",
txBase64,
configuration: SolanaSimulateTransactionConfig(
commitment: .confirmed,
encoding: .base64,
replaceRecentBlockhash: true
)!
)
)
return .proceed
} catch APIClientError.transactionSimulationError(let logs) {
} catch SolanaRPCError.transactionSimulationError(let logs) {
return blockedOutcome(
reason: "Phantom signed transaction failed simulation",
logs: logs,
extraMetadata: ["kind": "simulationErr"],
swapMetadata: swapMetadata
)
} catch APIClientError.responseError(let response) {
} catch SolanaRPCError.responseError(let response) {
var extra: [String: String] = ["kind": "preflightRejection"]
if let code = response.code { extra["code"] = "\(code)" }
if let message = response.message { extra["message"] = message }
Expand Down Expand Up @@ -562,26 +556,12 @@ public final class WalletConnection {
swapId: fundingSwapId.publicKey
)

let instructionsConverted = try instructions.map { instruction in
TransactionInstruction(
keys: try instruction.accounts.map { meta in
SolanaSwift.AccountMeta(
publicKey: try SolanaSwift.PublicKey(string: meta.publicKey.base58),
isSigner: meta.isSigner,
isWritable: meta.isWritable
)
},
programId: try SolanaSwift.PublicKey(string: instruction.program.base58),
data: [UInt8](instruction.data)
)
}

let recentBlockhash = try await rpc.getLatestBlockhash(commitment: "finalized")
let recentBlockhash = try await rpc.getLatestBlockhash(commitment: .finalized)

var transaction = Transaction(
instructions: instructionsConverted,
let transaction = SolanaTransaction(
payer: externalWallet,
recentBlockhash: recentBlockhash,
feePayer: try SolanaSwift.PublicKey(string: externalWallet.base58)
instructions: instructions
)

pendingSwap = PendingSwap(
Expand All @@ -591,7 +571,7 @@ public final class WalletConnection {
onCompleted: onCompleted
)

let txEncoded = Base58.fromBytes(Array(try transaction.serialize()))
let txEncoded = Base58.fromBytes(Array(transaction.encode()))

let payload: [String: Any] = [
"transaction": txEncoded,
Expand Down Expand Up @@ -845,15 +825,3 @@ extension WalletConnection {
static let mock = WalletConnection(owner: .mock, client: .mock)
}

// MARK: - WalletRPC -

/// The slice of Solana RPC that `WalletConnection` depends on. A narrow
/// protocol (instead of the full `SolanaAPIClient` surface) keeps test stubs
/// small and makes the dependency honest at the call site.
protocol WalletRPC {
func getLatestBlockhash(commitment: Commitment?) async throws -> String
func sendTransaction(transaction: String, configs: RequestConfiguration) async throws -> TransactionID
func simulateTransaction(transaction: String, configs: RequestConfiguration) async throws -> SimulationResult
}

extension JSONRPCAPIClient: WalletRPC {}
24 changes: 17 additions & 7 deletions FlipcashCore/Sources/FlipcashCore/Solana/LegacyMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ public struct LegacyMessage: Equatable, Sendable {
// MARK: - Init -

public init(accounts: [AccountMeta], recentBlockhash: Hash, instructions: [Instruction]) {
// Sort the account meta's based on:
// 1. Payer is always the first account / signer.
// 1. All signers are before non-signers.
// 2. Writable accounts before read-only accounts.
// 3. Programs last
let uniqueAccounts = accounts.filterUniqueAccounts().sorted()

// Canonical Solana legacy-message ordering:
// 1. Payer first.
// 2. Within signer/non-signer × writable/readonly groups, lex by pubkey.
// The sort intentionally does NOT use `AccountMeta.isProgram`. That flag
// is set heuristically (only on accounts added via `.program(publicKey:)`
// from `instructions[i].program`) and is false for transitive program
// references that arrive through `instructions[i].accounts`. Splitting
// readonly non-signers by that heuristic produces wire bytes that
// disagree with the canonical Solana order — verified against
// SolanaSwift's encoder via a byte-level parity test.
let uniqueAccounts = accounts.filterUniqueAccounts().sorted { lhs, rhs in
if lhs.isPayer != rhs.isPayer { return lhs.isPayer }
if lhs.isSigner != rhs.isSigner { return lhs.isSigner }
if lhs.isWritable != rhs.isWritable { return lhs.isWritable }
return lhs.publicKey.bytes.lexicographicallyPrecedes(rhs.publicKey.bytes)
}

let signers = uniqueAccounts.filter { $0.isSigner }
let readOnlySigners = uniqueAccounts.filter { !$0.isWritable && $0.isSigner }
let readOnly = uniqueAccounts.filter { !$0.isWritable && !$0.isSigner }
Expand Down
Loading