Skip to content

Commit c356380

Browse files
committed
feat(services/ocp): support VM::TransferForSwapWithFee instruction for buy new currency flow
Also simplify SwapRequest/SwapStartKind Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 9ded197 commit c356380

14 files changed

Lines changed: 270 additions & 33 deletions

File tree

services/opencode/src/androidTest/kotlin/com/getcode/opencode/solana/swap/SwapTests.kt

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class SwapInstructionsTest {
4242
private val mockSwapAuthority = generateRandomPublicKeyForTest() // The temporary swap authority
4343
private val mockRecentBlockhash = generateRandomPublicKeyForTest()
4444

45+
private val mockFeeDestination = generateRandomPublicKeyForTest()
46+
4547
// Mock VMs
4648
private val coreVmMetadata = VmMetadata(
4749
authority = vmAuthority,
@@ -348,19 +350,23 @@ class SwapInstructionsTest {
348350
seed = mockSeed,
349351
sellFeeBps = 100,
350352
vmLockDurationInDays = 21,
351-
alts = emptyList()
353+
alts = emptyList(),
354+
feeDestination = mockFeeDestination
352355
)
353356

354357
@Test
355358
fun testBuildNewCurrencyBuyInstructionsCount() {
356-
val amount = 100_000L
359+
val totalAmount = 100_000L
360+
val fee = 5_000L
361+
val amount = totalAmount - fee
357362

358363
val instructions = buildNewCurrencyBuyInstructions(
359364
serverParameters = mockNewCurrencyServerParams,
360365
nonce = mockNonce,
361366
authority = mockNewCurrencyAuthority,
362367
coreMintMetadata = coreMint,
363368
amount = amount,
369+
feeAmount = fee,
364370
)
365371

366372
// Expected Sequence (11 instructions):
@@ -381,14 +387,17 @@ class SwapInstructionsTest {
381387

382388
@Test
383389
fun testBuildNewCurrencyBuyInstructionPrograms() {
384-
val amount = 100_000L
390+
val totalAmount = 100_000L
391+
val fee = 5_000L
392+
val amount = totalAmount - fee
385393

386394
val instructions = buildNewCurrencyBuyInstructions(
387395
serverParameters = mockNewCurrencyServerParams,
388396
nonce = mockNonce,
389397
authority = mockNewCurrencyAuthority,
390398
coreMintMetadata = coreMint,
391399
amount = amount,
400+
feeAmount = fee,
392401
)
393402

394403
// 1. System::AdvanceNonce
@@ -427,14 +436,17 @@ class SwapInstructionsTest {
427436

428437
@Test
429438
fun testBuildNewCurrencyBuyInstructionAccounts() {
430-
val amount = 100_000L
439+
val totalAmount = 100_000L
440+
val fee = 5_000L
441+
val amount = totalAmount - fee
431442

432443
val instructions = buildNewCurrencyBuyInstructions(
433444
serverParameters = mockNewCurrencyServerParams,
434445
nonce = mockNonce,
435446
authority = mockNewCurrencyAuthority,
436447
coreMintMetadata = coreMint,
437448
amount = amount,
449+
feeAmount = fee,
438450
)
439451

440452
// Derive expected PDAs
@@ -494,9 +506,13 @@ class SwapInstructionsTest {
494506
assertEquals(expectedDepositAta, instructions[7].accounts[1].publicKey)
495507
assertEquals(derivedTargetMint, instructions[7].accounts[3].publicKey)
496508

497-
// 9. VM::TransferForSwap: vmAuthority at 0 matches core VM authority
509+
// 9. VM::TransferForSwapWithFee: vmAuthority at 0 matches core VM authority
498510
assertEquals(coreMint.vmMetadata.authority, instructions[8].accounts[0].publicKey)
499511
assertEquals(coreMint.vmMetadata.vm, instructions[8].accounts[1].publicKey)
512+
assertEquals(8, instructions[8].accounts.size)
513+
// feeDestination at index 6
514+
assertEquals(mockFeeDestination, instructions[8].accounts[6].publicKey)
515+
assertTrue(instructions[8].accounts[6].isWritable)
500516

501517
// 10. Reserve::BuyTokens: buyer at 0 is authority
502518
assertEquals(mockNewCurrencyAuthority, instructions[9].accounts[0].publicKey)
@@ -523,14 +539,17 @@ class SwapInstructionsTest {
523539

524540
@Test
525541
fun testBuildNewCurrencyBuyInstructionInitializeCurrencyData() {
526-
val amount = 100_000L
542+
val totalAmount = 100_000L
543+
val fee = 5_000L
544+
val amount = totalAmount - fee
527545

528546
val instructions = buildNewCurrencyBuyInstructions(
529547
serverParameters = mockNewCurrencyServerParams,
530548
nonce = mockNonce,
531549
authority = mockNewCurrencyAuthority,
532550
coreMintMetadata = coreMint,
533551
amount = amount,
552+
feeAmount = fee,
534553
)
535554

536555
// InitializeCurrency data: command(1) + name(32) + symbol(8) + seed(32) + bump(1) + mintBump(1) + padding(6) = 81 bytes
@@ -561,14 +580,17 @@ class SwapInstructionsTest {
561580

562581
@Test
563582
fun testBuildNewCurrencyBuyInstructionInitializePoolData() {
564-
val amount = 100_000L
583+
val totalAmount = 100_000L
584+
val fee = 5_000L
585+
val amount = totalAmount - fee
565586

566587
val instructions = buildNewCurrencyBuyInstructions(
567588
serverParameters = mockNewCurrencyServerParams,
568589
nonce = mockNonce,
569590
authority = mockNewCurrencyAuthority,
570591
coreMintMetadata = coreMint,
571592
amount = amount,
593+
feeAmount = fee,
572594
)
573595

574596
// InitializePool data: command(1) + sellFee(2) + bump(1) + vaultTargetBump(1) + vaultBaseBump(1) + padding(1) = 7 bytes
@@ -583,14 +605,17 @@ class SwapInstructionsTest {
583605

584606
@Test
585607
fun testBuildNewCurrencyBuyInstructionInitVmData() {
586-
val amount = 100_000L
608+
val totalAmount = 100_000L
609+
val fee = 5_000L
610+
val amount = totalAmount - fee
587611

588612
val instructions = buildNewCurrencyBuyInstructions(
589613
serverParameters = mockNewCurrencyServerParams,
590614
nonce = mockNonce,
591615
authority = mockNewCurrencyAuthority,
592616
coreMintMetadata = coreMint,
593617
amount = amount,
618+
feeAmount = fee,
594619
)
595620

596621
// InitVm data: command(1) + lockDuration(1) + vmBump(1) + vmOmnibusBump(1) = 4 bytes
@@ -602,24 +627,27 @@ class SwapInstructionsTest {
602627

603628
@Test
604629
fun testBuildNewCurrencyBuyInstructionBuyTokensData() {
605-
val amount = 100_000L
630+
val totalAmount = 100_000L
631+
val fee = 5_000L
632+
val amount = totalAmount - fee
606633

607634
val instructions = buildNewCurrencyBuyInstructions(
608635
serverParameters = mockNewCurrencyServerParams,
609636
nonce = mockNonce,
610637
authority = mockNewCurrencyAuthority,
611638
coreMintMetadata = coreMint,
612639
amount = amount,
640+
feeAmount = fee,
613641
)
614642

615643
// BuyTokens data: command(1) + inAmount(8) + minOutAmount(8) = 17 bytes
616644
val buyData = instructions[9].data
617645
assertEquals(17, buyData.size)
618646
assertEquals(CurrencyCreatorProgram.Command.buyTokens.value, buyData[0])
619647

620-
// inAmount = 100_000 = 0xA0860100_00000000 in LE
621-
assertEquals(0xA0.toByte(), buyData[1])
622-
assertEquals(0x86.toByte(), buyData[2])
648+
// inAmount = 95_000 = 0x18730100_00000000 in LE
649+
assertEquals(0x18.toByte(), buyData[1])
650+
assertEquals(0x73.toByte(), buyData[2])
623651
assertEquals(0x01.toByte(), buyData[3])
624652
assertEquals(0x00.toByte(), buyData[4])
625653

@@ -629,16 +657,56 @@ class SwapInstructionsTest {
629657
}
630658
}
631659

660+
@Test
661+
fun testBuildNewCurrencyBuyTransferForSwapWithFeeData() {
662+
val totalAmount = 100_000L
663+
val fee = 5_000L
664+
val amount = totalAmount - fee
665+
666+
val instructions = buildNewCurrencyBuyInstructions(
667+
serverParameters = mockNewCurrencyServerParams,
668+
nonce = mockNonce,
669+
authority = mockNewCurrencyAuthority,
670+
coreMintMetadata = coreMint,
671+
amount = amount,
672+
feeAmount = fee,
673+
)
674+
675+
// TransferForSwapWithFee data: command(1) + swapAmount(8) + feeAmount(8) + bump(1) = 18 bytes
676+
val transferData = instructions[8].data
677+
assertEquals(18, transferData.size)
678+
679+
// Command byte is transferForSwap (17)
680+
assertEquals(VirtualMachineProgram.Command.transferForSwap.value, transferData[0])
681+
682+
// swapAmount = 95_000 at bytes [1..8] in LE
683+
assertEquals(0x18.toByte(), transferData[1])
684+
assertEquals(0x73.toByte(), transferData[2])
685+
assertEquals(0x01.toByte(), transferData[3])
686+
for (i in 4..8) assertEquals(0x00.toByte(), transferData[i])
687+
688+
// feeAmount = 5_000 at bytes [9..16] in LE
689+
assertEquals(0x88.toByte(), transferData[9])
690+
assertEquals(0x13.toByte(), transferData[10])
691+
for (i in 11..16) assertEquals(0x00.toByte(), transferData[i])
692+
693+
// bump is last byte
694+
assertTrue(transferData[17] in Byte.MIN_VALUE..Byte.MAX_VALUE)
695+
}
696+
632697
@Test
633698
fun testNewCurrencyBuyInstructionAuthorityIsBuyer() {
634-
val amount = 50_000L
699+
val totalAmount = 50_000L
700+
val fee = 5_000L
701+
val amount = totalAmount - fee
635702

636703
val instructions = buildNewCurrencyBuyInstructions(
637704
serverParameters = mockNewCurrencyServerParams,
638705
nonce = mockNonce,
639706
authority = mockNewCurrencyAuthority,
640707
coreMintMetadata = coreMint,
641708
amount = amount,
709+
feeAmount = fee,
642710
)
643711

644712
// In the new currency flow, authority == buyer == swapAuthority

services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentSwap.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal class IntentSwap(
4949
authority = request.owner.authorityPublicKey,
5050
coreMintMetadata = Token.usdf,
5151
amount = request.swapAmount.underlyingTokenAmount.quarks,
52+
feeAmount = request.feeAmount?.underlyingTokenAmount?.quarks,
5253
)
5354
}
5455
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@ internal fun SwapRequest.currencyCreatorParams(): TransactionService.StatefulSwa
283283
setId(swapId.asSwapId())
284284
.setFromMint(details.fromMint.asSolanaAccountId())
285285
.setToMint(details.toMint.asSolanaAccountId())
286-
.setFeeAmount(details.feeAmount ?: 0)
287-
.setSwapAmount(details.swapAmount)
286+
.setSwapAmount(this@currencyCreatorParams.swapAmount.underlyingTokenAmount.quarks)
287+
.setFeeAmount(this@currencyCreatorParams.feeAmount?.underlyingTokenAmount?.quarks ?: 0)
288288
.apply {
289289
when (val source = details.fundingSource) {
290290
is SwapFundingSource.ExternalWallet -> {

services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/ProtobufToLocal.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ internal fun TransactionService.StatefulSwapResponse.ServerParameters.ReserveNew
167167
seed = seed.toPublicKey(),
168168
sellFeeBps = sellFeeBps,
169169
vmLockDurationInDays = vmLockDurationInDays,
170-
feeDestination = feeDestinationOrNull?.toPublicKey(),
170+
feeDestination = feeDestination.toPublicKey(),
171171
)
172172
}
173173

services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/funding/SwapFunding.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.getcode.opencode.internal.network.api.TransactionApi
44
import com.getcode.opencode.internal.network.api.intents.IntentFundSwap
55
import com.getcode.opencode.internal.network.executors.IntentExecutor
66
import com.getcode.opencode.model.accounts.AccountCluster
7+
import com.getcode.opencode.model.financial.LocalFiat
8+
import com.getcode.opencode.model.financial.plus
79
import com.getcode.opencode.model.transactions.SwapRequest
810
import com.getcode.opencode.solana.intents.IntentType
911
import com.getcode.solana.keys.PublicKey
@@ -21,7 +23,7 @@ internal class SwapFunding @Inject constructor(
2123
val fundingIntent = IntentFundSwap.create(
2224
intentId = PublicKey(request.fundingIntentId),
2325
sourceCluster = request.owner,
24-
amount = request.swapAmount,
26+
amount = request.swapAmount + (request.feeAmount ?: LocalFiat.Zero),
2527
fromMint = request.direction.sourceMint,
2628
verifiedState = request.verifiedState,
2729
)

services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/services/TransactionService.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import com.getcode.opencode.model.core.errors.GetIntentMetadataError
1818
import com.getcode.opencode.model.core.errors.GetLimitsError
1919
import com.getcode.opencode.model.core.errors.VoidGiftCardError
2020
import com.getcode.opencode.model.core.errors.WithdrawalAvailabilityError
21+
import com.getcode.opencode.model.financial.Fiat
2122
import com.getcode.opencode.model.financial.Limits
2223
import com.getcode.opencode.model.financial.LocalFiat
2324
import com.getcode.opencode.model.financial.Token
25+
import com.getcode.opencode.model.financial.minus
2426
import com.getcode.opencode.model.transactions.SwapDirection
2527
import com.getcode.opencode.model.transactions.SwapFundingSource
2628
import com.getcode.opencode.model.transactions.SwapRequest
@@ -188,18 +190,17 @@ internal class TransactionService @Inject constructor(
188190
val swapAuthority =
189191
if (isFreshlyLaunchedStub) owner.authority.keyPair else Ed25519.createKeyPair()
190192

193+
val netAmount = amount - (feeAmount ?: LocalFiat.Zero)
191194
val request = SwapRequest(
192195
owner = owner,
193196
swapAuthority = swapAuthority,
194197
kind = SwapStartKind.Reserve(
195198
fromMint = Mint.usdf,
196199
toMint = of.address,
197-
swapAmount = amount.underlyingTokenAmount.quarks,
198-
feeAmount = feeAmount?.underlyingTokenAmount?.quarks,
199200
fundingSource = source,
200201
),
201202
direction = SwapDirection.Buy(of),
202-
swapAmount = amount,
203+
swapAmount = netAmount,
203204
feeAmount = feeAmount,
204205
swapId = swapId ?: SwapId.generate(),
205206
verifiedState = verifiedState,
@@ -226,8 +227,6 @@ internal class TransactionService @Inject constructor(
226227
kind = SwapStartKind.Reserve(
227228
fromMint = of.address,
228229
toMint = Mint.usdf,
229-
swapAmount = amount.underlyingTokenAmount.quarks,
230-
feeAmount = null,
231230
fundingSource = source,
232231
),
233232
direction = SwapDirection.Sell(of),

services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/ExtractServerParameters.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ sealed interface ExtractedServerParams {
3333
val seed: PublicKey,
3434
val sellFeeBps: Int,
3535
val vmLockDurationInDays: Int,
36+
val feeDestination: PublicKey,
3637
): ExtractedServerParams
3738
}
3839

@@ -61,5 +62,6 @@ internal fun extractServerParameters(serverParameters: SwapResponseServerParamet
6162
seed = serverParameters.seed,
6263
sellFeeBps = serverParameters.sellFeeBps,
6364
vmLockDurationInDays = serverParameters.vmLockDurationInDays,
65+
feeDestination = serverParameters.feeDestination
6466
)
6567
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/VirtualMachineProgram.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal class VirtualMachineProgram {
1010
initVm(1),
1111
transferForSwap(17),
1212
closeSwapAccountIfEmpty(19),
13+
transferForSwapWithFee(20),
1314
;
1415
}
1516

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.getcode.opencode.internal.solana.programs
2+
3+
import com.getcode.opencode.solana.Instruction
4+
import com.getcode.solana.keys.AccountMeta
5+
import com.getcode.solana.keys.PublicKey
6+
import com.getcode.utils.bytes
7+
8+
internal class VirtualMachineProgram_TransferForSwapWithFee(
9+
private val vmAuthority: PublicKey,
10+
private val vm: PublicKey,
11+
private val swapper: PublicKey,
12+
private val swapPda: PublicKey,
13+
private val swapAta: PublicKey,
14+
private val destination: PublicKey,
15+
private val feeDestination: PublicKey,
16+
private val swapAmount: Long,
17+
private val feeAmount: Long,
18+
private val bump: Int
19+
): InstructionType {
20+
override fun instruction(): Instruction {
21+
return Instruction(
22+
program = VirtualMachineProgram.address,
23+
accounts = listOf(
24+
AccountMeta.writable(publicKey = vmAuthority, signer = true),
25+
AccountMeta.writable(publicKey = vm),
26+
AccountMeta.writable(publicKey = swapper, signer = true),
27+
28+
AccountMeta.readonly(publicKey = swapPda),
29+
30+
AccountMeta.writable(publicKey = swapAta),
31+
AccountMeta.writable(publicKey = destination),
32+
AccountMeta.writable(publicKey = feeDestination),
33+
34+
AccountMeta.readonly(publicKey = TokenProgram.address),
35+
),
36+
data = encode()
37+
)
38+
}
39+
40+
override fun encode(): List<Byte> {
41+
val data = mutableListOf<Byte>()
42+
data.add(VirtualMachineProgram.Command.transferForSwapWithFee.value)
43+
data.addAll(swapAmount.bytes)
44+
data.addAll(feeAmount.bytes)
45+
data.add(bump.toByte())
46+
47+
return data
48+
}
49+
}

0 commit comments

Comments
 (0)