Skip to content

Commit 12ec416

Browse files
committed
feat(ocp): add StatelessSwap RPC and consolidate swap errors
Add StatelessSwap bidirectional streaming RPC — proto definitions, API binding, executor, server parameter/result models, and protobuf mapping extensions. Wire it through TransactionService alongside the existing stateful swap (renamed internally to statefulSwap for clarity). Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 82e7f89 commit 12ec416

8 files changed

Lines changed: 527 additions & 5 deletions

File tree

definitions/opencode/protos/src/main/proto/transaction/v1/transaction_service.proto

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ service Transaction {
8181
// swap is funded.
8282
rpc StatefulSwap(stream StatefulSwapRequest) returns (stream StatefulSwapResponse);
8383

84+
// StatelessSwap is like StatefulSwap, but without a state management system and a
85+
// best-effort submission system.
86+
rpc StatelessSwap(stream StatelessSwapRequest) returns (stream StatelessSwapResponse);
87+
8488
// GetSwap gets metadata for a swap
8589
rpc GetSwap(GetSwapRequest) returns (GetSwapResponse);
8690

@@ -777,6 +781,176 @@ message StatefulSwapResponse {
777781
}
778782
}
779783

784+
message StatelessSwapRequest {
785+
oneof request {
786+
option (validate.required) = true;
787+
788+
Initiate initiate = 1;
789+
SubmitSignatures submit_signatures = 2;
790+
}
791+
792+
message Initiate {
793+
oneof kind {
794+
option (validate.required) = true;
795+
796+
CoinbaseStableSwapperClientParameters stablecoin = 1;
797+
}
798+
799+
// Client parameters for stateless swaps via the Coinbase Stable
800+
// Swapper program. Source funds are drawn from the owner's source-mint
801+
// ATA; destination is the owner's destination-mint VM Deposit ATA.
802+
message CoinbaseStableSwapperClientParameters {
803+
// The source mint that will be swapped from.
804+
common.v1.SolanaAccountId from_mint = 1 [(validate.rules).message.required = true];
805+
806+
807+
808+
// The destination mint that will be swapped to.
809+
common.v1.SolanaAccountId to_mint = 2 [(validate.rules).message.required = true];
810+
811+
812+
813+
// The amount to swap from the source mint in quarks.
814+
uint64 swap_amount = 3 [(validate.rules).uint64.gt = 0];
815+
816+
817+
}
818+
819+
// The owner account that owns the source ATA and the destination VM
820+
// Deposit ATA. The owner is the sole client-side signer of the swap
821+
// transaction.
822+
common.v1.SolanaAccountId owner = 2 [(validate.rules).message.required = true];
823+
824+
825+
826+
// If true, server waits until the swap transaction is finalized before
827+
// returning Success. If false, server returns Success as soon as the
828+
// transaction is submitted to the cluster.
829+
bool wait_for_finalization = 3;
830+
831+
// The signature is of serialize(StatelessSwapRequest.Initiate) without
832+
// this field set using the private key of the owner account. This
833+
// provides an authentication mechanism to the RPC.
834+
common.v1.Signature signature = 4 [(validate.rules).message.required = true];
835+
836+
837+
}
838+
839+
message SubmitSignatures {
840+
// The owner's signature over the locally constructed swap transaction.
841+
repeated common.v1.Signature transaction_signatures = 1 [(validate.rules).repeated = {
842+
min_items: 1
843+
max_items: 1
844+
}];
845+
846+
847+
}
848+
}
849+
850+
message StatelessSwapResponse {
851+
oneof response {
852+
option (validate.required) = true;
853+
854+
ServerParameters server_parameters = 1;
855+
Success success = 2;
856+
Error error = 3;
857+
}
858+
859+
message ServerParameters {
860+
oneof kind {
861+
option (validate.required) = true;
862+
863+
CoinbaseStableSwapperServerParameter stablecoin = 1;
864+
}
865+
866+
// Server parameters for executing stateless swap flows against the
867+
// Coinbase Stable Swapper program.
868+
//
869+
// Supported Solana transaction version: v0
870+
//
871+
// Instruction format:
872+
// 1. [Optional] ComputeBudget::SetComputeUnitLimit
873+
// 2. [Optional] ComputeBudget::SetComputeUnitPrice
874+
// 3. [Optional] Memo::Memo
875+
// 4. AssociatedTokenAccount::CreateIdempotent (open owner's to_mint VM Deposit ATA)
876+
// 5. CoinbaseStableSwapper::Swap (owner's from_mint ATA -> owner's to_mint VM Deposit ATA)
877+
message CoinbaseStableSwapperServerParameter {
878+
// Subsidizer account that will pay the transaction fee.
879+
common.v1.SolanaAccountId payer = 1 [(validate.rules).message.required = true];
880+
881+
882+
883+
// The Solana blockhash to set on the transaction. This is a
884+
// regular recent blockhash, not a durable nonce.
885+
common.v1.Blockhash blockhash = 2 [(validate.rules).message.required = true];
886+
887+
888+
889+
// ALTs that should be used when constructing the versioned transaction
890+
repeated common.v1.SolanaAddressLookupTable alts = 3;
891+
892+
// Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit
893+
// instruction. If the value is 0, then the instruction can be omitted.
894+
uint32 compute_unit_limit = 4;
895+
896+
// Compute unit price provided in the ComputeBudget::SetComputeUnitPrice
897+
// instruction. If the value is 0, then the instruction can be omitted.
898+
uint64 compute_unit_price = 5;
899+
900+
// Value provided into the Memo::Memo instruction. If the value length is 0,
901+
// then the instruction can be omitted.
902+
string memo_value = 6 [(validate.rules).string.max_len = 64];
903+
904+
905+
906+
// The CoinbaseStableSwapper liquidity pool's configured fee recipient,
907+
// sourced from the on-chain LiquidityPool account. Required by the
908+
// CoinbaseStableSwapper::Swap instruction.
909+
common.v1.SolanaAccountId pool_fee_recipient = 7 [(validate.rules).message.required = true];
910+
911+
912+
}
913+
}
914+
915+
message Success {
916+
Code code = 1;
917+
enum Code {
918+
// Transaction was forwarded to the cluster. Returned when
919+
// wait_for_finalization = false.
920+
SUBMITTED = 0;
921+
// Transaction was finalized on-chain. Returned when
922+
// wait_for_finalization = true.
923+
FINALIZED = 1;
924+
}
925+
926+
// The signature of the submitted swap transaction. Clients may use
927+
// this to look up the transaction on-chain.
928+
common.v1.Signature transaction_signature = 2 [(validate.rules).message.required = true];
929+
930+
931+
}
932+
933+
message Error {
934+
Code code = 1;
935+
enum Code {
936+
// Denied by a guard (spam, money laundering, etc)
937+
DENIED = 0;
938+
// There is an issue with the provided transaction signature
939+
SIGNATURE_ERROR = 1;
940+
// The swap parameters failed server-side validation (eg.
941+
// unsupported mint pair, insufficient source balance, swap amount
942+
// out of allowed range)
943+
INVALID_SWAP = 2;
944+
// The transaction was submitted but reverted on-chain, or its
945+
// blockhash expired before confirmation. Only relevant when
946+
// wait_for_finalization = true.
947+
TRANSACTION_FAILED = 3;
948+
}
949+
950+
repeated ErrorDetails error_details = 2;
951+
}
952+
}
953+
780954
message GetSwapRequest {
781955
common.v1.SwapId id = 1 [(validate.rules).message.required = true];
782956

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,8 @@ class TransactionApi @Inject constructor(
245245
fun swap(
246246
requestFlow: Flow<TransactionService.StatefulSwapRequest>
247247
): Flow<TransactionService.StatefulSwapResponse> = api.statefulSwap(requestFlow)
248+
249+
fun statelessSwap(
250+
requestFlow: Flow<TransactionService.StatelessSwapRequest>
251+
): Flow<TransactionService.StatelessSwapResponse> = api.statelessSwap(requestFlow)
248252
}

0 commit comments

Comments
 (0)