Skip to content
Merged

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

2 changes: 1 addition & 1 deletion FlipcashAPI/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ enum ContractPackage: String, CaseIterable {
/// The pinned version consumed when this package isn't building against a local checkout.
var version: Version {
switch self {
case .ocp: return "0.8.0"
case .ocp: return "0.9.0"
case .flipcash2: return "0.20.0"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,7 @@ extension SwapService {
// serialization time, per `SwiftProtobuf.Message.sign(with:)`).
let initiate = Ocp_Transaction_V1_StatelessSwapRequest.with {
$0.initiate = .with {
$0.kind = .stablecoin(.with {
$0.fromMint = fromMint.address.solanaAccountID
$0.toMint = toMint.address.solanaAccountID
$0.swapAmount = amount.quarks
})
$0.kind = .stablecoin(.coinbaseStableSwapper(fromMint: fromMint, toMint: toMint, amount: amount))
$0.owner = owner.publicKey.solanaAccountID
$0.waitForFinalization = true
$0.signature = $0.sign(with: owner)
Expand All @@ -201,6 +197,24 @@ extension SwapService {
}
}

extension Ocp_Transaction_V1_StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters {

/// Initiate parameters for a stateless Coinbase swap, stamped with the version of the
/// instruction `TransactionBuilder.statelessSwap` builds so the two cannot drift.
static func coinbaseStableSwapper(
fromMint: MintMetadata,
toMint: MintMetadata,
amount: TokenAmount
) -> Self {
.with {
$0.fromMint = fromMint.address.solanaAccountID
$0.toMint = toMint.address.solanaAccountID
$0.swapAmount = amount.quarks
$0.swapVersion = CoinbaseStableSwapperProgram.SwapV2.version
}
}
}

/// Wraps an async completion so it can only fire once. Used to funnel the
/// response handler and the stream-status handler into the same `Result`
/// without double-resuming the upstream continuation.
Expand Down
2 changes: 2 additions & 0 deletions FlipcashCore/Sources/FlipcashCore/Models/SwapModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ extension VerifiedSwapMetadata.ClientParameters {
$0.fundingSource = fundingSource.protoSource
$0.fundingID = fundingSource.fundingID
$0.destinationOwner = destinationOwner.solanaAccountID
// Must match the instruction `TransactionBuilder.swapUsdfToUsdc` builds.
$0.swapVersion = CoinbaseStableSwapperProgram.SwapV2.version
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,16 @@ extension CoinbaseStableSwapperProgram {
)
}

/// PDA: ["address_whitelist"] @ CoinbaseStableSwapperProgram
public static func deriveWhitelistAddress() -> ProgramDerivedAccount? {
ProgramDerivedAccount.findProgramAddress(
seeds: [Data("address_whitelist".utf8)],
program: address
)
}

/// The pool-side accounts a `Swap` instruction targets for a mint pair.
/// The pool-side accounts a `SwapV2` instruction targets for a mint pair.
public struct SwapAccounts: Equatable, Sendable {
public let pool: PublicKey
public let inVault: PublicKey
public let outVault: PublicKey
public let inVaultTokenAccount: PublicKey
public let outVaultTokenAccount: PublicKey
public let whitelist: PublicKey
}

/// Derives every pool-side account `Swap` needs for the given mint pair,
/// Derives every pool-side account `SwapV2` needs for the given mint pair,
/// or `nil` when any derivation fails.
public static func deriveSwapAccounts(
fromMint: PublicKey,
Expand All @@ -67,8 +58,7 @@ extension CoinbaseStableSwapperProgram {
let inVault = deriveTokenVaultAddress(pool: pool.publicKey, mint: fromMint),
let outVault = deriveTokenVaultAddress(pool: pool.publicKey, mint: toMint),
let inVaultTokenAccount = deriveVaultTokenAccountAddress(vault: inVault.publicKey),
let outVaultTokenAccount = deriveVaultTokenAccountAddress(vault: outVault.publicKey),
let whitelist = deriveWhitelistAddress()
let outVaultTokenAccount = deriveVaultTokenAccountAddress(vault: outVault.publicKey)
else {
return nil
}
Expand All @@ -78,8 +68,7 @@ extension CoinbaseStableSwapperProgram {
inVault: inVault.publicKey,
outVault: outVault.publicKey,
inVaultTokenAccount: inVaultTokenAccount.publicKey,
outVaultTokenAccount: outVaultTokenAccount.publicKey,
whitelist: whitelist.publicKey
outVaultTokenAccount: outVaultTokenAccount.publicKey
)
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
//
// CoinbaseStableSwapperProgram.Swap.swift
// CoinbaseStableSwapperProgram.SwapV2.swift
// FlipcashCore
//

import Foundation

extension CoinbaseStableSwapperProgram {

/// Anchor-format instruction for the Coinbase Stable Swapper's `swap` handler.
/// Anchor-format instruction for the Coinbase Stable Swapper's `swap_v2` handler.
///
/// Account list (16, in this exact order):
/// Takes no whitelist account (the retired v1 `swap` handler had one).
///
/// Account list (15, in this exact order):
/// 0. [] pool
/// 1. [] inVault
/// 2. [] outVault
Expand All @@ -22,12 +24,14 @@ extension CoinbaseStableSwapperProgram {
/// 9. [] fromMint
/// 10. [] toMint
/// 11. [WRITE, SIGNER] user
/// 12. [] whitelist
/// 13. [] TokenProgram
/// 14. [] AssociatedTokenProgram
/// 15. [] SystemProgram
/// 12. [] TokenProgram
/// 13. [] AssociatedTokenProgram
/// 14. [] SystemProgram
///
public struct Swap {
public struct SwapV2 {

/// The `swap_version` a request must carry so the server builds the same instruction.
public static let version: UInt32 = 2

public let pool: PublicKey
public let inVault: PublicKey
Expand All @@ -41,12 +45,11 @@ extension CoinbaseStableSwapperProgram {
public let fromMint: PublicKey
public let toMint: PublicKey
public let user: PublicKey
public let whitelist: PublicKey
public let amountIn: UInt64
public let minAmountOut: UInt64

/// Anchor discriminator for `swap`: sha256("global:swap")[0..8]
public static let discriminator: [UInt8] = [248, 198, 158, 145, 225, 117, 135, 200]
/// Anchor discriminator for `swap_v2`: sha256("global:swap_v2")[0..8]
public static let discriminator: [UInt8] = [43, 4, 237, 11, 26, 201, 30, 98]

public init(
pool: PublicKey,
Expand All @@ -61,7 +64,6 @@ extension CoinbaseStableSwapperProgram {
fromMint: PublicKey,
toMint: PublicKey,
user: PublicKey,
whitelist: PublicKey,
amountIn: UInt64,
minAmountOut: UInt64
) {
Expand All @@ -77,7 +79,6 @@ extension CoinbaseStableSwapperProgram {
self.fromMint = fromMint
self.toMint = toMint
self.user = user
self.whitelist = whitelist
self.amountIn = amountIn
self.minAmountOut = minAmountOut
}
Expand All @@ -86,7 +87,7 @@ extension CoinbaseStableSwapperProgram {

// MARK: - InstructionType -

extension CoinbaseStableSwapperProgram.Swap: InstructionType {
extension CoinbaseStableSwapperProgram.SwapV2: InstructionType {

public init(instruction: Instruction) throws {
throw CommandParseError.instructionMismatch
Expand All @@ -108,7 +109,6 @@ extension CoinbaseStableSwapperProgram.Swap: InstructionType {
.readonly(publicKey: fromMint),
.readonly(publicKey: toMint),
.writable(publicKey: user, signer: true),
.readonly(publicKey: whitelist),
.readonly(publicKey: TokenProgram.address),
.readonly(publicKey: AssociatedTokenProgram.address),
.readonly(publicKey: SystemProgram.address),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension SwapInstructionBuilder {
/// 2. [Optional] ComputeBudget::SetComputeUnitPrice
/// 3. [Optional] Memo::Memo
/// 4. AssociatedTokenAccount::CreateIdempotent (open owner's to_mint VM Deposit ATA)
/// 5. CoinbaseStableSwapper::Swap (owner's from_mint ATA -> owner's to_mint VM Deposit ATA)
/// 5. CoinbaseStableSwapper::SwapV2 (owner's from_mint ATA -> owner's to_mint VM Deposit ATA)
/// ```
///
/// Used by the on-app-open USDC → USDF sweep: source is the owner's plain
Expand Down Expand Up @@ -122,13 +122,13 @@ extension SwapInstructionBuilder {
).instruction()
)

// 5. CoinbaseStableSwapper::Swap — owner's from-mint ATA → owner's to-mint VM Deposit ATA.
// 5. CoinbaseStableSwapper::SwapV2 — owner's from-mint ATA → owner's to-mint VM Deposit ATA.
// `minAmountOut: amount` enforces a 1:1 rate; the Coinbase Stable
// Swapper is a stable pair so any slippage indicates a misconfigured
// pool and the swap should fail server-side rather than silently
// lose value.
instructions.append(
CoinbaseStableSwapperProgram.Swap(
CoinbaseStableSwapperProgram.SwapV2(
pool: swapAccounts.pool,
inVault: swapAccounts.inVault,
outVault: swapAccounts.outVault,
Expand All @@ -141,7 +141,6 @@ extension SwapInstructionBuilder {
fromMint: fromMint.address,
toMint: toMint.address,
user: owner,
whitelist: swapAccounts.whitelist,
amountIn: amount,
minAmountOut: amount
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ extension SwapInstructionBuilder {
MemoProgram.Memo(message: swapId.base58).instruction()
)

// 7. Swap (sender's USDC ATA → sender's USDF ATA)
// 7. Swap (sender's USDC ATA → sender's USDF ATA). The Coinbase pool uses
// `swap_v2`, which dropped the whitelist account.
switch pool {
case .usdf:
instructions.append(
Expand Down Expand Up @@ -151,7 +152,7 @@ extension SwapInstructionBuilder {
// misconfigured pool and the swap should fail rather than
// silently lose value.
instructions.append(
CoinbaseStableSwapperProgram.Swap(
CoinbaseStableSwapperProgram.SwapV2(
pool: swapAccounts.pool,
inVault: swapAccounts.inVault,
outVault: swapAccounts.outVault,
Expand All @@ -164,7 +165,6 @@ extension SwapInstructionBuilder {
fromMint: .usdc,
toMint: .usdf,
user: sender,
whitelist: swapAccounts.whitelist,
amountIn: amount,
minAmountOut: amount
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ extension SwapInstructionBuilder {
).instruction()
)

// 8. CoinbaseStableSwapper::Swap
// 8. CoinbaseStableSwapper::SwapV2
// Swaps USDF in the swap-authority's ATA for USDC into the destination-owner's ATA.
instructions.append(
CoinbaseStableSwapperProgram.Swap(
CoinbaseStableSwapperProgram.SwapV2(
pool: swapAccounts.pool,
inVault: swapAccounts.inVault,
outVault: swapAccounts.outVault,
Expand All @@ -141,7 +141,6 @@ extension SwapInstructionBuilder {
fromMint: fromMintMetadata.address,
toMint: toMintMetadata.address,
user: swapAuthority,
whitelist: swapAccounts.whitelist,
amountIn: amount,
minAmountOut: minOutput
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import Testing
/// offset in `PoolAccount` yields a valid-looking `PublicKey` and a transaction that encodes
/// fine, so only the end-to-end bytes catch it.
///
/// The fixture below was accepted by `simulateTransaction` on mainnet (no error, 68922 compute
/// units, `Swapped 5000000 tokens`). Reading `fee_recipient` at the offset the published IDL
/// implies instead fails the same simulation with `ConstraintAddress` (2012).
/// The deposit swap is `swap_v2` (15 accounts). These bytes were accepted by
/// `simulateTransaction` on mainnet (no error, 34752 compute units, `Swapped 5000000 tokens`).
/// Reading `fee_recipient` at the offset the published IDL implies instead failed that
/// simulation with `ConstraintAddress` (2012).
@Suite("CoinbaseStableSwapper deposit transaction bytes")
struct CoinbaseStableSwapperDepositTransactionTests {

Expand All @@ -38,8 +39,9 @@ struct CoinbaseStableSwapperDepositTransactionTests {
private static let amount: UInt64 = 5_000_000

/// Any change here is a wire-format change on the deposit path. Re-simulate before updating.
/// Mainnet-simulated `swap_v2` bytes; see the suite comment.
private static let expectedBase64 = """
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
"""

// MARK: - Tests
Expand Down Expand Up @@ -72,6 +74,8 @@ struct CoinbaseStableSwapperDepositTransactionTests {
)

#expect(instructions.count == 8)
#expect(instructions[6].data.prefix(8).elementsEqual(CoinbaseStableSwapperProgram.SwapV2.discriminator))
#expect(instructions[6].accounts.count == 15)
#expect(transaction.encode() == (try #require(Data(base64Encoded: Self.expectedBase64))))
}

Expand Down
Loading
Loading