Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ service Transaction {
// swap is funded.
rpc StatefulSwap(stream StatefulSwapRequest) returns (stream StatefulSwapResponse);

// StatelessSwap is like StatefulSwap, but without a state management system and a
// best-effort submission system.
rpc StatelessSwap(stream StatelessSwapRequest) returns (stream StatelessSwapResponse);

// GetSwap gets metadata for a swap
rpc GetSwap(GetSwapRequest) returns (GetSwapResponse);

Expand Down Expand Up @@ -777,6 +781,176 @@ message StatefulSwapResponse {
}
}

message StatelessSwapRequest {
oneof request {
option (validate.required) = true;

Initiate initiate = 1;
SubmitSignatures submit_signatures = 2;
}

message Initiate {
oneof kind {
option (validate.required) = true;

CoinbaseStableSwapperClientParameters stablecoin = 1;
}

// Client parameters for stateless swaps via the Coinbase Stable
// Swapper program. Source funds are drawn from the owner's source-mint
// ATA; destination is the owner's destination-mint VM Deposit ATA.
message CoinbaseStableSwapperClientParameters {
// The source mint that will be swapped from.
common.v1.SolanaAccountId from_mint = 1 [(validate.rules).message.required = true];



// The destination mint that will be swapped to.
common.v1.SolanaAccountId to_mint = 2 [(validate.rules).message.required = true];



// The amount to swap from the source mint in quarks.
uint64 swap_amount = 3 [(validate.rules).uint64.gt = 0];


}

// The owner account that owns the source ATA and the destination VM
// Deposit ATA. The owner is the sole client-side signer of the swap
// transaction.
common.v1.SolanaAccountId owner = 2 [(validate.rules).message.required = true];



// If true, server waits until the swap transaction is finalized before
// returning Success. If false, server returns Success as soon as the
// transaction is submitted to the cluster.
bool wait_for_finalization = 3;

// The signature is of serialize(StatelessSwapRequest.Initiate) without
// this field set using the private key of the owner account. This
// provides an authentication mechanism to the RPC.
common.v1.Signature signature = 4 [(validate.rules).message.required = true];


}

message SubmitSignatures {
// The owner's signature over the locally constructed swap transaction.
repeated common.v1.Signature transaction_signatures = 1 [(validate.rules).repeated = {
min_items: 1
max_items: 1
}];


}
}

message StatelessSwapResponse {
oneof response {
option (validate.required) = true;

ServerParameters server_parameters = 1;
Success success = 2;
Error error = 3;
}

message ServerParameters {
oneof kind {
option (validate.required) = true;

CoinbaseStableSwapperServerParameter stablecoin = 1;
}

// Server parameters for executing stateless swap flows against the
// Coinbase Stable Swapper program.
//
// Supported Solana transaction version: v0
//
// Instruction format:
// 1. [Optional] ComputeBudget::SetComputeUnitLimit
// 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)
message CoinbaseStableSwapperServerParameter {
// Subsidizer account that will pay the transaction fee.
common.v1.SolanaAccountId payer = 1 [(validate.rules).message.required = true];



// The Solana blockhash to set on the transaction. This is a
// regular recent blockhash, not a durable nonce.
common.v1.Blockhash blockhash = 2 [(validate.rules).message.required = true];



// ALTs that should be used when constructing the versioned transaction
repeated common.v1.SolanaAddressLookupTable alts = 3;

// Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit
// instruction. If the value is 0, then the instruction can be omitted.
uint32 compute_unit_limit = 4;

// Compute unit price provided in the ComputeBudget::SetComputeUnitPrice
// instruction. If the value is 0, then the instruction can be omitted.
uint64 compute_unit_price = 5;

// Value provided into the Memo::Memo instruction. If the value length is 0,
// then the instruction can be omitted.
string memo_value = 6 [(validate.rules).string.max_len = 64];



// The CoinbaseStableSwapper liquidity pool's configured fee recipient,
// sourced from the on-chain LiquidityPool account. Required by the
// CoinbaseStableSwapper::Swap instruction.
common.v1.SolanaAccountId pool_fee_recipient = 7 [(validate.rules).message.required = true];


}
}

message Success {
Code code = 1;
enum Code {
// Transaction was forwarded to the cluster. Returned when
// wait_for_finalization = false.
SUBMITTED = 0;
// Transaction was finalized on-chain. Returned when
// wait_for_finalization = true.
FINALIZED = 1;
}

// The signature of the submitted swap transaction. Clients may use
// this to look up the transaction on-chain.
common.v1.Signature transaction_signature = 2 [(validate.rules).message.required = true];


}

message Error {
Code code = 1;
enum Code {
// Denied by a guard (spam, money laundering, etc)
DENIED = 0;
// There is an issue with the provided transaction signature
SIGNATURE_ERROR = 1;
// The swap parameters failed server-side validation (eg.
// unsupported mint pair, insufficient source balance, swap amount
// out of allowed range)
INVALID_SWAP = 2;
// The transaction was submitted but reverted on-chain, or its
// blockhash expired before confirmation. Only relevant when
// wait_for_finalization = true.
TRANSACTION_FAILED = 3;
}

repeated ErrorDetails error_details = 2;
}
}

message GetSwapRequest {
common.v1.SwapId id = 1 [(validate.rules).message.required = true];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.getcode.opencode.model.financial.LaunchpadMetadata
import com.getcode.opencode.model.financial.MintMetadata
import com.getcode.opencode.model.financial.VmMetadata
import com.getcode.opencode.model.financial.usdf
import com.getcode.opencode.model.transactions.SwapResponseServerParameters
import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters
import com.getcode.opencode.tests.generateRandomPublicKeyForTest
import com.getcode.opencode.utils.generate
import com.getcode.solana.keys.Mint
Expand Down Expand Up @@ -89,7 +89,7 @@ class SwapInstructionsTest {
)

// Mock Server Response (Stateless for simplicity)
private val mockServerParams = SwapResponseServerParameters.ExistingCurrency(
private val mockServerParams = StatefulSwapResponseServerParameters.ExistingCurrency(
payer = mockPayer,
blockhash = mockRecentBlockhash,
nonce = mockNonce,
Expand Down Expand Up @@ -337,7 +337,7 @@ class SwapInstructionsTest {
private val mockNewCurrencyAuthority = generateRandomPublicKeyForTest()
private val mockSeed = generateRandomPublicKeyForTest()

private val mockNewCurrencyServerParams = SwapResponseServerParameters.NewCurrency(
private val mockNewCurrencyServerParams = StatefulSwapResponseServerParameters.NewCurrency(
payer = mockPayer,
blockhash = mockRecentBlockhash,
nonce = mockNonce,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.transactions.ExchangeData
import com.getcode.opencode.model.transactions.SwapFundingSource
import com.getcode.opencode.model.transactions.SwapMetadata
import com.getcode.opencode.model.transactions.SwapRequest
import com.getcode.opencode.model.transactions.StatefulSwapRequest
import com.getcode.opencode.model.transactions.SwapState
import com.getcode.opencode.model.transactions.TransactionMetadata
import com.getcode.opencode.model.transactions.WithdrawalAvailability
Expand Down Expand Up @@ -269,7 +269,7 @@ class TransactionController @Inject constructor(
swapId: SwapId?,
of: Token,
source: SwapFundingSource,
fund: (suspend (SwapRequest) -> Result<Unit>)?,
fund: (suspend (StatefulSwapRequest) -> Result<Unit>)?,
): Result<SwapId> {
trace("Starting ${amount.localFiat.nativeAmount.formatted()} buy of ${of.symbol}")
val tokenizedOwner = owner.withTimelockForToken(of)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.transactions.SwapFundingSource
import com.getcode.opencode.model.transactions.SwapMetadata
import com.getcode.opencode.model.transactions.SwapRequest
import com.getcode.opencode.model.transactions.StatefulSwapRequest
import com.getcode.opencode.model.transactions.SwapState
import com.getcode.opencode.model.transactions.WithdrawalAvailability
import com.getcode.opencode.solana.intents.IntentType
Expand All @@ -35,7 +35,7 @@ interface TransactionOperations {
swapId: SwapId? = null,
of: Token,
source: SwapFundingSource = SwapFundingSource.SubmitIntent(),
fund: (suspend (SwapRequest) -> Result<Unit>)? = null,
fund: (suspend (StatefulSwapRequest) -> Result<Unit>)? = null,
): Result<SwapId>

suspend fun sell(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.getcode.opencode.model.financial.Limits
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.transactions.SwapFundingSource
import com.getcode.opencode.model.transactions.SwapRequest
import com.getcode.opencode.model.transactions.StatefulSwapRequest
import com.getcode.opencode.model.transactions.TransactionMetadata
import com.getcode.opencode.model.transactions.WithdrawalAvailability
import com.getcode.opencode.model.core.errors.SubmitIntentError
Expand Down Expand Up @@ -73,7 +73,7 @@ internal class InternalTransactionRepository @Inject constructor(
swapId: SwapId?,
verifiedState: VerifiedState,
source: SwapFundingSource,
fund: (suspend (SwapRequest) -> Result<Unit>)?
fund: (suspend (StatefulSwapRequest) -> Result<Unit>)?
): Result<SwapId> = service.buy(
scope = scope,
swapId = swapId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,8 @@ class TransactionApi @Inject constructor(
fun swap(
requestFlow: Flow<TransactionService.StatefulSwapRequest>
): Flow<TransactionService.StatefulSwapResponse> = api.statefulSwap(requestFlow)

fun statelessSwap(
requestFlow: Flow<TransactionService.StatelessSwapRequest>
): Flow<TransactionService.StatelessSwapResponse> = api.statelessSwap(requestFlow)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,55 @@ import com.getcode.opencode.internal.network.extensions.stablecoinParams
import com.getcode.opencode.internal.network.extensions.verifiedMetadata
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.financial.usdf
import com.getcode.opencode.model.transactions.SwapRequest
import com.getcode.opencode.model.transactions.SwapResponseServerParameters
import com.getcode.opencode.model.transactions.StatefulSwapRequest
import com.getcode.opencode.model.transactions.StatefulSwapResponseServerParameters
import com.getcode.opencode.model.transactions.SwapStartKind
import com.getcode.opencode.model.transactions.VerifiedSwapMetadata
import com.getcode.opencode.solana.SolanaTransaction
import com.getcode.opencode.solana.TransactionBuilder
import com.getcode.solana.keys.Signature

internal class IntentSwap(
val request: SwapRequest,
internal class IntentStatefulSwap(
val request: StatefulSwapRequest,
val metadata: VerifiedSwapMetadata,
){
var parameters: SwapResponseServerParameters? = null
var parameters: StatefulSwapResponseServerParameters? = null

fun sign(parameters: SwapResponseServerParameters): List<Signature> {
fun sign(parameters: StatefulSwapResponseServerParameters): List<Signature> {
val transaction = transaction(parameters)
return when (parameters) {
is SwapResponseServerParameters.ExistingCurrency -> {
is StatefulSwapResponseServerParameters.ExistingCurrency -> {
transaction.signatures(request.owner.authority.keyPair, request.swapAuthority)
}
is SwapResponseServerParameters.NewCurrency -> {
is StatefulSwapResponseServerParameters.NewCurrency -> {
// For new currency, owner == swapAuthority, so only 1 unique signature needed
transaction.signatures(request.owner.authority.keyPair)
}

is SwapResponseServerParameters.Stablecoin -> {
is StatefulSwapResponseServerParameters.Stablecoin -> {
transaction.signatures(request.owner.authority.keyPair, request.swapAuthority)
}
}
}

fun transaction(parameters: SwapResponseServerParameters): SolanaTransaction {
fun transaction(parameters: StatefulSwapResponseServerParameters): SolanaTransaction {
return when (parameters) {
is SwapResponseServerParameters.ExistingCurrency -> TransactionBuilder.swap(
is StatefulSwapResponseServerParameters.ExistingCurrency -> TransactionBuilder.swap(
response = parameters,
authority = request.owner.authorityPublicKey,
swapAuthority = request.swapAuthority.toPublicKey(),
direction = request.direction,
amount = request.swapAmount.underlyingTokenAmount.quarks,
)
is SwapResponseServerParameters.NewCurrency -> TransactionBuilder.buyNewCurrency(
is StatefulSwapResponseServerParameters.NewCurrency -> TransactionBuilder.buyNewCurrency(
response = parameters,
authority = request.owner.authorityPublicKey,
coreMintMetadata = Token.usdf,
amount = request.swapAmount.underlyingTokenAmount.quarks,
feeAmount = request.feeAmount?.underlyingTokenAmount?.quarks,
)

is SwapResponseServerParameters.Stablecoin -> TransactionBuilder.stablecoinSwap(
is StatefulSwapResponseServerParameters.Stablecoin -> TransactionBuilder.stablecoinSwap(
response = parameters,
authority = request.owner.authorityPublicKey,
swapAuthority = request.swapAuthority.toPublicKey(),
Expand Down
Loading
Loading