From 42153df517ef0cfed43cd1a1f1b0b1ff3e2d133e Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 22 Oct 2026 11:20:59 -0400 Subject: [PATCH 1/7] feat(swap): build Coinbase SwapV2 and request swap_version 2 Add the SwapV2 instruction (15 accounts, no whitelist) and use it for the server-mediated USDF->USDC withdraw and stateless USDC->USDF sweep. Both requests set swapVersion from SwapV2.version, the same constant the instruction type owns, so the request and the locally built transaction flip together. The external-wallet deposit path keeps the v1 Swap. --- .../Services/SwapService+StatelessSwap.swift | 24 +++- .../FlipcashCore/Models/SwapModels.swift | 2 + .../CoinbaseStableSwapperProgram.SwapV2.swift | 127 +++++++++++++++++ ...SwapInstructionBuilder+StatelessSwap.swift | 7 +- .../SwapInstructionBuilder+UsdfToUsdc.swift | 5 +- .../CoinbaseStableSwapperSwapV2Tests.swift | 128 ++++++++++++++++++ ...wapInstructionBuilderUsdfToUsdcTests.swift | 16 ++- 7 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift create mode 100644 FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift diff --git a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/SwapService+StatelessSwap.swift b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/SwapService+StatelessSwap.swift index 56eefbf50..e219a773a 100644 --- a/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/SwapService+StatelessSwap.swift +++ b/FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/SwapService+StatelessSwap.swift @@ -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) @@ -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. diff --git a/FlipcashCore/Sources/FlipcashCore/Models/SwapModels.swift b/FlipcashCore/Sources/FlipcashCore/Models/SwapModels.swift index deb8f6bf3..9648c07ab 100644 --- a/FlipcashCore/Sources/FlipcashCore/Models/SwapModels.swift +++ b/FlipcashCore/Sources/FlipcashCore/Models/SwapModels.swift @@ -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 } } } diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift new file mode 100644 index 000000000..6e6a7d06c --- /dev/null +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift @@ -0,0 +1,127 @@ +// +// CoinbaseStableSwapperProgram.SwapV2.swift +// FlipcashCore +// + +import Foundation + +extension CoinbaseStableSwapperProgram { + + /// Anchor-format instruction for the Coinbase Stable Swapper's `swap_v2` handler. + /// + /// Same arguments as `Swap`, minus the deprecated whitelist account. + /// + /// Account list (15, in this exact order): + /// 0. [] pool + /// 1. [] inVault + /// 2. [] outVault + /// 3. [WRITE] inVaultTokenAccount + /// 4. [WRITE] outVaultTokenAccount + /// 5. [WRITE] userFromTokenAccount + /// 6. [WRITE] toTokenAccount + /// 7. [WRITE] feeRecipientTokenAccount + /// 8. [] feeRecipient + /// 9. [] fromMint + /// 10. [] toMint + /// 11. [WRITE, SIGNER] user + /// 12. [] TokenProgram + /// 13. [] AssociatedTokenProgram + /// 14. [] SystemProgram + /// + 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 + public let outVault: PublicKey + public let inVaultTokenAccount: PublicKey + public let outVaultTokenAccount: PublicKey + public let userFromTokenAccount: PublicKey + public let toTokenAccount: PublicKey + public let feeRecipientTokenAccount: PublicKey + public let feeRecipient: PublicKey + public let fromMint: PublicKey + public let toMint: PublicKey + public let user: PublicKey + public let amountIn: UInt64 + public let minAmountOut: UInt64 + + /// 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, + inVault: PublicKey, + outVault: PublicKey, + inVaultTokenAccount: PublicKey, + outVaultTokenAccount: PublicKey, + userFromTokenAccount: PublicKey, + toTokenAccount: PublicKey, + feeRecipientTokenAccount: PublicKey, + feeRecipient: PublicKey, + fromMint: PublicKey, + toMint: PublicKey, + user: PublicKey, + amountIn: UInt64, + minAmountOut: UInt64 + ) { + self.pool = pool + self.inVault = inVault + self.outVault = outVault + self.inVaultTokenAccount = inVaultTokenAccount + self.outVaultTokenAccount = outVaultTokenAccount + self.userFromTokenAccount = userFromTokenAccount + self.toTokenAccount = toTokenAccount + self.feeRecipientTokenAccount = feeRecipientTokenAccount + self.feeRecipient = feeRecipient + self.fromMint = fromMint + self.toMint = toMint + self.user = user + self.amountIn = amountIn + self.minAmountOut = minAmountOut + } + } +} + +// MARK: - InstructionType - + +extension CoinbaseStableSwapperProgram.SwapV2: InstructionType { + + public init(instruction: Instruction) throws { + throw CommandParseError.instructionMismatch + } + + public func instruction() -> Instruction { + Instruction( + program: CoinbaseStableSwapperProgram.address, + accounts: [ + .readonly(publicKey: pool), + .readonly(publicKey: inVault), + .readonly(publicKey: outVault), + .writable(publicKey: inVaultTokenAccount), + .writable(publicKey: outVaultTokenAccount), + .writable(publicKey: userFromTokenAccount), + .writable(publicKey: toTokenAccount), + .writable(publicKey: feeRecipientTokenAccount), + .readonly(publicKey: feeRecipient), + .readonly(publicKey: fromMint), + .readonly(publicKey: toMint), + .writable(publicKey: user, signer: true), + .readonly(publicKey: TokenProgram.address), + .readonly(publicKey: AssociatedTokenProgram.address), + .readonly(publicKey: SystemProgram.address), + ], + data: encode() + ) + } + + public func encode() -> Data { + var data = Data() + data.append(contentsOf: Self.discriminator) + data.append(contentsOf: amountIn.bytes) + data.append(contentsOf: minAmountOut.bytes) + return data + } +} diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift index c20c1bdee..30d494cc2 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift @@ -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 @@ -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, @@ -141,7 +141,6 @@ extension SwapInstructionBuilder { fromMint: fromMint.address, toMint: toMint.address, user: owner, - whitelist: swapAccounts.whitelist, amountIn: amount, minAmountOut: amount ).instruction() diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift index 11adfbcdc..b9f7da720 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift @@ -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, @@ -141,7 +141,6 @@ extension SwapInstructionBuilder { fromMint: fromMintMetadata.address, toMint: toMintMetadata.address, user: swapAuthority, - whitelist: swapAccounts.whitelist, amountIn: amount, minAmountOut: minOutput ).instruction() diff --git a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift new file mode 100644 index 000000000..4924ebb0d --- /dev/null +++ b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift @@ -0,0 +1,128 @@ +// +// CoinbaseStableSwapperSwapV2Tests.swift +// FlipcashCoreTests +// + +import Foundation +import Testing +import OCPClientProtocol +@testable import FlipcashCore + +/// Golden vector shared with Android and the server's `instructions_swap_v2_test.go`. +@Suite("CoinbaseStableSwapper SwapV2 instruction") +struct CoinbaseStableSwapperSwapV2Tests { + + private static func key(_ i: Int) -> PublicKey { + var bytes = [UInt8](repeating: 0, count: 32) + bytes[0] = UInt8(i + 1) + return try! PublicKey(bytes) + } + + private static let amountIn: UInt64 = 1_500_000 + private static let minAmountOut: UInt64 = 1_490_000 + + private static var v1: Instruction { + CoinbaseStableSwapperProgram.Swap( + pool: key(0), inVault: key(1), outVault: key(2), + inVaultTokenAccount: key(3), outVaultTokenAccount: key(4), + userFromTokenAccount: key(5), toTokenAccount: key(6), + feeRecipientTokenAccount: key(7), feeRecipient: key(8), + fromMint: key(9), toMint: key(10), user: key(11), whitelist: key(12), + amountIn: amountIn, minAmountOut: minAmountOut + ).instruction() + } + + private static var v2: Instruction { + CoinbaseStableSwapperProgram.SwapV2( + pool: key(0), inVault: key(1), outVault: key(2), + inVaultTokenAccount: key(3), outVaultTokenAccount: key(4), + userFromTokenAccount: key(5), toTokenAccount: key(6), + feeRecipientTokenAccount: key(7), feeRecipient: key(8), + fromMint: key(9), toMint: key(10), user: key(11), + amountIn: amountIn, minAmountOut: minAmountOut + ).instruction() + } + + @Test("Data matches the golden vector exactly") + func data_goldenVector() { + let expected: [UInt8] = [ + 43, 4, 237, 11, 26, 201, 30, 98, + 0x60, 0xe3, 0x16, 0, 0, 0, 0, 0, + 0x50, 0xbc, 0x16, 0, 0, 0, 0, 0, + ] + #expect(Array(Self.v2.data) == expected) + } + + @Test("Args are identical to v1") + func data_matchesV1Args() { + #expect(Self.v1.data.dropFirst(8) == Self.v2.data.dropFirst(8)) + } + + @Test("Accounts are v1's with the whitelist (index 12) removed") + func accounts_v1WithoutWhitelist() { + let v1 = Self.v1.accounts + let v2 = Self.v2.accounts + #expect(v1.count == 16) + #expect(v1[12].publicKey == Self.key(12)) + #expect(v2.count == 15) + + var expected = v1 + expected.remove(at: 12) + #expect(v2.map(\.publicKey) == expected.map(\.publicKey)) + #expect(v2.map(\.isSigner) == expected.map(\.isSigner)) + #expect(v2.map(\.isWritable) == expected.map(\.isWritable)) + } + + @Test("Targets the Coinbase Stable Swapper program") + func program() { + #expect(Self.v2.program == CoinbaseStableSwapperProgram.address) + } + + @Test("Stateful stablecoin client parameters carry swapVersion 2") + func statefulClientParameters_carryVersion() { + let params = VerifiedSwapMetadata.ClientParameters( + id: SwapId.generate(), + fromMint: MintMetadata.usdf.address, + toMint: MintMetadata.usdc.address, + amount: TokenAmount(quarks: 1_000_000, mint: MintMetadata.usdf.address), + fundingSource: .submitIntent(id: Self.key(0)), + kind: .stablecoin(destinationOwner: Self.key(1)) + ) + #expect(params.stablecoinProto.swapVersion == 2) + } + + @Test("Stateless stablecoin initiate parameters carry swapVersion 2") + func statelessInitiate_carriesVersion() { + let params = Ocp_Transaction_V1_StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters + .coinbaseStableSwapper( + fromMint: .usdc, + toMint: .usdf, + amount: TokenAmount(quarks: 1_000_000, mint: MintMetadata.usdc.address) + ) + #expect(params.swapVersion == 2) + } + + @Test("Stateless builder emits SwapV2 (15 accounts, v2 discriminator)") + func statelessBuilder_emitsV2() { + let serverParams = StatelessSwapServerParameters( + payer: Self.key(20), + blockhash: try! Hash([UInt8](repeating: 3, count: 32)), + alts: [], + computeUnitLimit: 0, + computeUnitPrice: 0, + memoValue: "", + poolFeeRecipient: Self.key(21) + ) + let ixs = SwapInstructionBuilder.buildStatelessSwapInstructions( + serverParameters: serverParams, + owner: Self.key(22), + fromMint: .usdc, + toMint: .usdf, + amount: 1_000_000 + ) + let swap = ixs.last! + #expect(swap.program == CoinbaseStableSwapperProgram.address) + #expect(swap.accounts.count == 15) + #expect(Array(swap.data.prefix(8)) == CoinbaseStableSwapperProgram.SwapV2.discriminator) + } +} diff --git a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift index 33ecce970..350aba297 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift @@ -159,7 +159,7 @@ struct SwapInstructionBuilderUsdfToUsdcTests { #expect(ix.accounts[6].publicKey == Self.feeDestination) } - @Test("Instruction 7 is CoinbaseStableSwapper Swap") + @Test("Instruction 7 is CoinbaseStableSwapper SwapV2") func instruction7_isCoinbaseSwap() { let ix = Self.makeInstructions()[7] #expect(ix.program == CoinbaseStableSwapperProgram.address) @@ -248,11 +248,15 @@ struct SwapInstructionBuilderUsdfToUsdcTests { #expect(ix.accounts[11].isSigner == true) } - @Test("Swap whitelist (account 12) is the Coinbase whitelist PDA") - func swap_whitelist() { + @Test("Swap has 15 accounts and no whitelist; programs follow the user") + func swap_accountsHaveNoWhitelist() { let ix = Self.makeInstructions()[7] - let expected = CoinbaseStableSwapperProgram.deriveWhitelistAddress()!.publicKey - #expect(ix.accounts[12].publicKey == expected) + let whitelist = CoinbaseStableSwapperProgram.deriveWhitelistAddress()!.publicKey + #expect(ix.accounts.count == 15) + #expect(!ix.accounts.contains { $0.publicKey == whitelist }) + #expect(ix.accounts[12].publicKey == TokenProgram.address) + #expect(ix.accounts[13].publicKey == AssociatedTokenProgram.address) + #expect(ix.accounts[14].publicKey == SystemProgram.address) } // MARK: - Discriminator + data layout (24 bytes) @@ -260,7 +264,7 @@ struct SwapInstructionBuilderUsdfToUsdcTests { @Test("Swap instruction data starts with the Anchor discriminator") func swap_discriminator() { let ix = Self.makeInstructions()[7] - let expected: [UInt8] = [248, 198, 158, 145, 225, 117, 135, 200] + let expected: [UInt8] = [43, 4, 237, 11, 26, 201, 30, 98] #expect(Array(ix.data.prefix(8)) == expected) } From cae3c525bb9a39215550dec080c77e41cadbe120 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 22 Oct 2026 12:51:05 -0400 Subject: [PATCH 2/7] chore(deps): bump ocp-client-protocol to 0.9.0 0.9.0 carries swap_version on both CoinbaseStableSwapper initiate params. --- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- FlipcashAPI/Package.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 02241c907..f19253844 100644 --- a/Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "967b87f053de503ec7757ad7d9e6ebc7bb6a3b87ae116109d6b2630591c572f0", + "originHash" : "c1a366f97e033b7ee60810d9f4229a153bddc573d336d834f2446e421324d519", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -213,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/code-payments/ocp-client-protocol", "state" : { - "revision" : "01b4f913f076e03732d7aed0e19a689cce14ab07", - "version" : "0.8.0" + "revision" : "831b209e37c482143b8da2b0f850ed534755bf10", + "version" : "0.9.0" } }, { diff --git a/FlipcashAPI/Package.swift b/FlipcashAPI/Package.swift index 7912cf3b8..866a969a8 100644 --- a/FlipcashAPI/Package.swift +++ b/FlipcashAPI/Package.swift @@ -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" } } From afd897eb079184dfb310877045e73a9aa45a16e4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 22 Oct 2026 14:18:06 -0400 Subject: [PATCH 3/7] feat(swap): build SwapV2 for the external-wallet deposit swap The client-built USDC to USDF deposit swap now emits CoinbaseStableSwapper swap_v2 (15 accounts, no whitelist) instead of the legacy swap, matching the server-mediated paths. Test vectors updated; the deposit transaction bytes are builder-derived. --- .../Solana/SwapInstructionBuilder+UsdcToUsdf.swift | 6 +++--- ...nbaseStableSwapperDepositTransactionTests.swift | 14 ++++++++++---- .../SwapInstructionBuilderUsdcToUsdfTests.swift | 9 ++++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift index 820ccbcc9..4719cbc5f 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift @@ -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( @@ -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, @@ -164,7 +165,6 @@ extension SwapInstructionBuilder { fromMint: .usdc, toMint: .usdf, user: sender, - whitelist: swapAccounts.whitelist, amountIn: amount, minAmountOut: amount ).instruction() diff --git a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift index dd3b35d77..cbae8e050 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift @@ -15,9 +15,12 @@ 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, no whitelist). The expected bytes are +/// builder-derived: there is no on-chain `swap_v2` capture for this transaction yet. The +/// previous `swap` (v1) version of these bytes was accepted by `simulateTransaction` on mainnet +/// (no error, 68922 compute units, `Swapped 5000000 tokens`), and reading `fee_recipient` at the +/// offset the published IDL implies instead failed that simulation with `ConstraintAddress` +/// (2012). Re-simulate the v2 bytes before relying on them as a mainnet-accepted vector. @Suite("CoinbaseStableSwapper deposit transaction bytes") struct CoinbaseStableSwapperDepositTransactionTests { @@ -38,8 +41,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. + /// Builder-derived `swap_v2` bytes; see the suite comment. private static let expectedBase64 = """ - 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 """ // MARK: - Tests @@ -72,6 +76,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)))) } diff --git a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift index 60f3acb51..763f4cd7c 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift @@ -262,7 +262,7 @@ struct SwapInstructionBuilderUsdcToUsdfTests { #expect(coinbase.data == legacy.data) } - @Test("Coinbase swap accounts: pool PDAs, sender ATAs, fee recipient, whitelist") + @Test("Coinbase swap accounts: pool PDAs, sender ATAs, fee recipient, no whitelist") func coinbase_swapAccounts() throws { let instructions = Self.makeCoinbaseInstructions() let ix = instructions[6] @@ -272,14 +272,13 @@ struct SwapInstructionBuilderUsdcToUsdfTests { let outVault = try #require(CoinbaseStableSwapperProgram.deriveTokenVaultAddress(pool: pool, mint: .usdf)).publicKey let inVaultTokenAccount = try #require(CoinbaseStableSwapperProgram.deriveVaultTokenAccountAddress(vault: inVault)).publicKey let outVaultTokenAccount = try #require(CoinbaseStableSwapperProgram.deriveVaultTokenAccountAddress(vault: outVault)).publicKey - let whitelist = try #require(CoinbaseStableSwapperProgram.deriveWhitelistAddress()).publicKey let senderUsdcAta = instructions[4].accounts[1].publicKey let senderUsdfAta = instructions[2].accounts[1].publicKey let feeRecipientUsdcAta = try #require( PublicKey.deriveAssociatedAccount(from: Self.feeRecipient, mint: .usdc) ).publicKey - // Account list order documented on CoinbaseStableSwapperProgram.Swap. + // Account list order documented on CoinbaseStableSwapperProgram.SwapV2. #expect(ix.accounts[0].publicKey == pool) #expect(ix.accounts[1].publicKey == inVault) #expect(ix.accounts[2].publicKey == outVault) @@ -293,14 +292,14 @@ struct SwapInstructionBuilderUsdcToUsdfTests { #expect(ix.accounts[10].publicKey == PublicKey.usdf) #expect(ix.accounts[11].publicKey == Self.sender) #expect(ix.accounts[11].isSigner) - #expect(ix.accounts[12].publicKey == whitelist) + #expect(ix.accounts.count == 15) } @Test("Coinbase swap data: discriminator + amountIn(8 LE) + minAmountOut(8 LE), both equal to amount") func coinbase_dataLayout() { let ix = Self.makeCoinbaseInstructions(amount: 20_000_000)[6] #expect(ix.data.count == 8 + 8 + 8) - #expect(Array(ix.data.prefix(8)) == CoinbaseStableSwapperProgram.Swap.discriminator) + #expect(Array(ix.data.prefix(8)) == CoinbaseStableSwapperProgram.SwapV2.discriminator) #expect(UInt64(bytes: Array(ix.data[8..<16])) == 20_000_000) #expect(UInt64(bytes: Array(ix.data[16..<24])) == 20_000_000) } From fd401b6963b2aede94bd721d83d51dfa4ef4fb7f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 22 Oct 2026 14:24:24 -0400 Subject: [PATCH 4/7] refactor(swap): stop deriving the unused Coinbase whitelist for SwapV2 SwapV2 has no whitelist account, so deriveSwapAccounts no longer derives or returns it. deriveWhitelistAddress() stays for the v1 Swap golden test and the no-whitelist assertion. --- .../Programs/CoinbaseStableSwapperProgram+PDAs.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift index f5565b6bd..d2938f4a9 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift @@ -46,17 +46,16 @@ extension CoinbaseStableSwapperProgram { ) } - /// 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, @@ -67,8 +66,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 } @@ -78,8 +76,7 @@ extension CoinbaseStableSwapperProgram { inVault: inVault.publicKey, outVault: outVault.publicKey, inVaultTokenAccount: inVaultTokenAccount.publicKey, - outVaultTokenAccount: outVaultTokenAccount.publicKey, - whitelist: whitelist.publicKey + outVaultTokenAccount: outVaultTokenAccount.publicKey ) } } From 3e5b3fabe98e67941329e85c64d90da4b5cfc74b Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 22 Oct 2026 14:44:24 -0400 Subject: [PATCH 5/7] refactor(swap): remove the v1 CoinbaseStableSwapper Swap instruction --- .../CoinbaseStableSwapperProgram+PDAs.swift | 8 -- .../CoinbaseStableSwapperProgram.Swap.swift | 127 ------------------ .../CoinbaseStableSwapperProgram.SwapV2.swift | 2 +- .../CoinbaseStableSwapperSwapV2Tests.swift | 37 ++--- ...wapInstructionBuilderUsdfToUsdcTests.swift | 2 - 5 files changed, 9 insertions(+), 167 deletions(-) delete mode 100644 FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift index d2938f4a9..d24c613d5 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift @@ -38,14 +38,6 @@ 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 `SwapV2` instruction targets for a mint pair. public struct SwapAccounts: Equatable, Sendable { public let pool: PublicKey diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift deleted file mode 100644 index b8591987c..000000000 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// CoinbaseStableSwapperProgram.Swap.swift -// FlipcashCore -// - -import Foundation - -extension CoinbaseStableSwapperProgram { - - /// Anchor-format instruction for the Coinbase Stable Swapper's `swap` handler. - /// - /// Account list (16, in this exact order): - /// 0. [] pool - /// 1. [] inVault - /// 2. [] outVault - /// 3. [WRITE] inVaultTokenAccount - /// 4. [WRITE] outVaultTokenAccount - /// 5. [WRITE] userFromTokenAccount - /// 6. [WRITE] toTokenAccount - /// 7. [WRITE] feeRecipientTokenAccount - /// 8. [] feeRecipient - /// 9. [] fromMint - /// 10. [] toMint - /// 11. [WRITE, SIGNER] user - /// 12. [] whitelist - /// 13. [] TokenProgram - /// 14. [] AssociatedTokenProgram - /// 15. [] SystemProgram - /// - public struct Swap { - - public let pool: PublicKey - public let inVault: PublicKey - public let outVault: PublicKey - public let inVaultTokenAccount: PublicKey - public let outVaultTokenAccount: PublicKey - public let userFromTokenAccount: PublicKey - public let toTokenAccount: PublicKey - public let feeRecipientTokenAccount: PublicKey - public let feeRecipient: PublicKey - 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] - - public init( - pool: PublicKey, - inVault: PublicKey, - outVault: PublicKey, - inVaultTokenAccount: PublicKey, - outVaultTokenAccount: PublicKey, - userFromTokenAccount: PublicKey, - toTokenAccount: PublicKey, - feeRecipientTokenAccount: PublicKey, - feeRecipient: PublicKey, - fromMint: PublicKey, - toMint: PublicKey, - user: PublicKey, - whitelist: PublicKey, - amountIn: UInt64, - minAmountOut: UInt64 - ) { - self.pool = pool - self.inVault = inVault - self.outVault = outVault - self.inVaultTokenAccount = inVaultTokenAccount - self.outVaultTokenAccount = outVaultTokenAccount - self.userFromTokenAccount = userFromTokenAccount - self.toTokenAccount = toTokenAccount - self.feeRecipientTokenAccount = feeRecipientTokenAccount - self.feeRecipient = feeRecipient - self.fromMint = fromMint - self.toMint = toMint - self.user = user - self.whitelist = whitelist - self.amountIn = amountIn - self.minAmountOut = minAmountOut - } - } -} - -// MARK: - InstructionType - - -extension CoinbaseStableSwapperProgram.Swap: InstructionType { - - public init(instruction: Instruction) throws { - throw CommandParseError.instructionMismatch - } - - public func instruction() -> Instruction { - Instruction( - program: CoinbaseStableSwapperProgram.address, - accounts: [ - .readonly(publicKey: pool), - .readonly(publicKey: inVault), - .readonly(publicKey: outVault), - .writable(publicKey: inVaultTokenAccount), - .writable(publicKey: outVaultTokenAccount), - .writable(publicKey: userFromTokenAccount), - .writable(publicKey: toTokenAccount), - .writable(publicKey: feeRecipientTokenAccount), - .readonly(publicKey: feeRecipient), - .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), - ], - data: encode() - ) - } - - public func encode() -> Data { - var data = Data() - data.append(contentsOf: Self.discriminator) - data.append(contentsOf: amountIn.bytes) - data.append(contentsOf: minAmountOut.bytes) - return data - } -} diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift index 6e6a7d06c..2a023ece8 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.SwapV2.swift @@ -9,7 +9,7 @@ extension CoinbaseStableSwapperProgram { /// Anchor-format instruction for the Coinbase Stable Swapper's `swap_v2` handler. /// - /// Same arguments as `Swap`, minus the deprecated whitelist account. + /// Takes no whitelist account (the retired v1 `swap` handler had one). /// /// Account list (15, in this exact order): /// 0. [] pool diff --git a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift index 4924ebb0d..8e1c99258 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperSwapV2Tests.swift @@ -21,17 +21,6 @@ struct CoinbaseStableSwapperSwapV2Tests { private static let amountIn: UInt64 = 1_500_000 private static let minAmountOut: UInt64 = 1_490_000 - private static var v1: Instruction { - CoinbaseStableSwapperProgram.Swap( - pool: key(0), inVault: key(1), outVault: key(2), - inVaultTokenAccount: key(3), outVaultTokenAccount: key(4), - userFromTokenAccount: key(5), toTokenAccount: key(6), - feeRecipientTokenAccount: key(7), feeRecipient: key(8), - fromMint: key(9), toMint: key(10), user: key(11), whitelist: key(12), - amountIn: amountIn, minAmountOut: minAmountOut - ).instruction() - } - private static var v2: Instruction { CoinbaseStableSwapperProgram.SwapV2( pool: key(0), inVault: key(1), outVault: key(2), @@ -53,24 +42,14 @@ struct CoinbaseStableSwapperSwapV2Tests { #expect(Array(Self.v2.data) == expected) } - @Test("Args are identical to v1") - func data_matchesV1Args() { - #expect(Self.v1.data.dropFirst(8) == Self.v2.data.dropFirst(8)) - } - - @Test("Accounts are v1's with the whitelist (index 12) removed") - func accounts_v1WithoutWhitelist() { - let v1 = Self.v1.accounts - let v2 = Self.v2.accounts - #expect(v1.count == 16) - #expect(v1[12].publicKey == Self.key(12)) - #expect(v2.count == 15) - - var expected = v1 - expected.remove(at: 12) - #expect(v2.map(\.publicKey) == expected.map(\.publicKey)) - #expect(v2.map(\.isSigner) == expected.map(\.isSigner)) - #expect(v2.map(\.isWritable) == expected.map(\.isWritable)) + @Test("Accounts match the golden 15-account list, in order, with only the user signing") + func accounts_golden() { + let accounts = Self.v2.accounts + #expect(accounts.map(\.publicKey) == (0...11).map(Self.key) + [ + TokenProgram.address, AssociatedTokenProgram.address, SystemProgram.address, + ]) + #expect(accounts.map(\.isSigner) == (0..<15).map { $0 == 11 }) + #expect(accounts.map(\.isWritable) == (0..<15).map { [3, 4, 5, 6, 7, 11].contains($0) }) } @Test("Targets the Coinbase Stable Swapper program") diff --git a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift index 350aba297..047831fa9 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift @@ -251,9 +251,7 @@ struct SwapInstructionBuilderUsdfToUsdcTests { @Test("Swap has 15 accounts and no whitelist; programs follow the user") func swap_accountsHaveNoWhitelist() { let ix = Self.makeInstructions()[7] - let whitelist = CoinbaseStableSwapperProgram.deriveWhitelistAddress()!.publicKey #expect(ix.accounts.count == 15) - #expect(!ix.accounts.contains { $0.publicKey == whitelist }) #expect(ix.accounts[12].publicKey == TokenProgram.address) #expect(ix.accounts[13].publicKey == AssociatedTokenProgram.address) #expect(ix.accounts[14].publicKey == SystemProgram.address) From c2fab0a14c4b6b8109d96fb974abc12229716522 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 22 Oct 2026 15:22:19 -0400 Subject: [PATCH 6/7] docs(swap): drop the stale whitelist note from the deposit vector comment --- .../CoinbaseStableSwapperDepositTransactionTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift index cbae8e050..029e11dfb 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift @@ -15,7 +15,7 @@ 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 deposit swap is `swap_v2` (15 accounts, no whitelist). The expected bytes are +/// The deposit swap is `swap_v2` (15 accounts). The expected bytes are /// builder-derived: there is no on-chain `swap_v2` capture for this transaction yet. The /// previous `swap` (v1) version of these bytes was accepted by `simulateTransaction` on mainnet /// (no error, 68922 compute units, `Swapped 5000000 tokens`), and reading `fee_recipient` at the From af354cb5a30aba5c0c56ea881b674ffa5deaad5f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 22 Oct 2026 15:33:02 -0400 Subject: [PATCH 7/7] docs(swap): record the mainnet simulation of the swap_v2 deposit vector --- ...oinbaseStableSwapperDepositTransactionTests.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift index 029e11dfb..dd0b4b893 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperDepositTransactionTests.swift @@ -15,12 +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 deposit swap is `swap_v2` (15 accounts). The expected bytes are -/// builder-derived: there is no on-chain `swap_v2` capture for this transaction yet. The -/// previous `swap` (v1) version of these bytes was accepted by `simulateTransaction` on mainnet -/// (no error, 68922 compute units, `Swapped 5000000 tokens`), and reading `fee_recipient` at the -/// offset the published IDL implies instead failed that simulation with `ConstraintAddress` -/// (2012). Re-simulate the v2 bytes before relying on them as a mainnet-accepted vector. +/// 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 { @@ -41,7 +39,7 @@ 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. - /// Builder-derived `swap_v2` bytes; see the suite comment. + /// Mainnet-simulated `swap_v2` bytes; see the suite comment. private static let expectedBase64 = """ AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA4VAKL0PLpNizvAoz6QRhxwuE+0xI1gTXBS8xC2iFAT9YAIIEbBeYxANDSqQKEPw/bim+87xyfndOcR1Z8MXipwRQhN3zo5EjvB3buXfyApNPeiOya5xLRmU4amnmybe8G0ZvC2QXkvoK+rzDvts+c808FFOCj9M+Gw1VaTwlFFQuCIGuzkYUNbUsQHluSoCkWyRnCKUYxj6p9wIBIqXQISoIsQLEV0nZ6/A9+EnNdx0SgwdXMN9srCBKAsQrgkmUgy/UHEeoQ9iL75ZI2OJCdbgb8cuXN12zjqg9u576KCFfMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAABUpTUPhdyILWFKVWcniKKW3fHqur0KYGeIhJMvTu9qAGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpDEFZLOrgP4XlL2Cp/OoiptfGnqhwN8d2wUpZw0Pc/EQXrHn35X2ZhEB0QDeeHO4EVQ94zdMsrvVy1q+XDO+XnCuKEXJELwMYhHcwB4CqMwj+zD6MwkrlErJ/0Le6FUYqNPjPxJe2tVN8GFmPsvNI9OAKkeiLpyN8+pe9+U4m9EE908SAij1Ps5+uycukm1pjSlLt4RVI9SSqIPLNRu+AcYyXJY9OJInxuz0QKRSODYMLWhOZ2v8QhASOe9jb6fhZsAv1H/nJezKQ1MsvZdgtk2ADZkchGXY0muvR9OrxRba9RisE/6dQ2FdZLElujNnDg858Wn5R9em05xQ2Tb0ZL8b6evO+2606PWXzaqvJdDGxu+TC0vbg5HymAgNFL11hY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MICAAFAkANAwAIAAkD6AMAAAAAAAARBwAEABAHCwoBAREHAAUTEAcLCgEBEQcAAwAUBwsKAQEJACtjR2ZIaUM2S2dnM0ZwRlp2Z3dHY3N3c0NSdHA0YUJQMmZ6dVhSUVBpenVODA8SDQ4BAgMEBg8UEAALEQcYKwTtCxrJHmJAS0wAAAAAAEBLTAAAAAAACwMEBQAJA0BLTAAAAAAA """