From cf93d693b718df87ca829f254733ae76880a4626 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 30 Apr 2026 15:00:57 -0400 Subject: [PATCH 1/6] Add support for Coinbase onramp swap funding source --- go.mod | 2 +- go.sum | 4 +-- ocp/data/swap/swap.go | 1 + ocp/rpc/transaction/server.go | 6 ++++ ocp/rpc/transaction/swap.go | 59 ++++++++++++++++++++++++++++++++++- ocp/worker/swap/config.go | 5 +++ ocp/worker/swap/runtime.go | 4 +++ ocp/worker/swap/util.go | 55 ++++++++++++++++++++++++++++++-- ocp/worker/swap/worker.go | 40 ++++++++++++++++++++++++ 9 files changed, 169 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index eed0527..320aae0 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( filippo.io/edwards25519 v1.1.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/code-payments/code-vm-indexer v1.2.0 - github.com/code-payments/ocp-protobuf-api v1.10.0 + github.com/code-payments/ocp-protobuf-api v1.10.1-0.20260429180655-b59b2a1ebfc1 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.2.1 github.com/golang/protobuf v1.5.4 diff --git a/go.sum b/go.sum index 9f926e7..b7fe773 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-vm-indexer v1.2.0 h1:rSHpBMiT9BKgmKcXg/VIoi/h0t7jNxGx07Qz59m+6Q0= github.com/code-payments/code-vm-indexer v1.2.0/go.mod h1:vn91YN2qNqb+gGJeZe2+l+TNxVmEEiRHXXnIn2Y40h8= -github.com/code-payments/ocp-protobuf-api v1.10.0 h1:8GEDLh3NShOYz6J7a9VOCqu+xJSd7xR42pewaPfkiE4= -github.com/code-payments/ocp-protobuf-api v1.10.0/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= +github.com/code-payments/ocp-protobuf-api v1.10.1-0.20260429180655-b59b2a1ebfc1 h1:zBF2FPaIwuKP6wPDw/rciXTvzCxZm0ELLcUyFYJ1fCw= +github.com/code-payments/ocp-protobuf-api v1.10.1-0.20260429180655-b59b2a1ebfc1/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/ocp/data/swap/swap.go b/ocp/data/swap/swap.go index 3b1adc8..517e6c9 100644 --- a/ocp/data/swap/swap.go +++ b/ocp/data/swap/swap.go @@ -25,6 +25,7 @@ const ( FundingSourceUnknown = iota FundingSourceSubmitIntent FundingSourceExternalWallet + FundingSourceCoinbaseOnramp ) type Kind uint8 diff --git a/ocp/rpc/transaction/server.go b/ocp/rpc/transaction/server.go index e5ba1fa..3add3c8 100644 --- a/ocp/rpc/transaction/server.go +++ b/ocp/rpc/transaction/server.go @@ -9,6 +9,7 @@ import ( transactionpb "github.com/code-payments/ocp-protobuf-api/generated/go/transaction/v1" + "github.com/code-payments/ocp-server/coinbase" "github.com/code-payments/ocp-server/ocp/aml" "github.com/code-payments/ocp-server/ocp/antispam" auth_util "github.com/code-payments/ocp-server/ocp/auth" @@ -35,6 +36,8 @@ type transactionServer struct { antispamGuard *antispam.Guard amlGuard *aml.Guard + coinbaseClient *coinbase.Client + nodeID string noncePools []*transaction.LocalNoncePool @@ -53,6 +56,7 @@ func NewTransactionServer( submitIntentIntegration integration.SubmitIntent, antispamGuard *antispam.Guard, amlGuard *aml.Guard, + coinbaseClient *coinbase.Client, nodeID string, noncePools []*transaction.LocalNoncePool, configProvider ConfigProvider, @@ -100,6 +104,8 @@ func NewTransactionServer( antispamGuard: antispamGuard, amlGuard: amlGuard, + coinbaseClient: coinbaseClient, + nodeID: nodeID, noncePools: noncePools, diff --git a/ocp/rpc/transaction/swap.go b/ocp/rpc/transaction/swap.go index 7643ed6..c80a4e0 100644 --- a/ocp/rpc/transaction/swap.go +++ b/ocp/rpc/transaction/swap.go @@ -5,6 +5,8 @@ import ( "context" "crypto/ed25519" "database/sql" + "math/big" + "strings" "time" "github.com/mr-tron/base58/base58" @@ -16,6 +18,7 @@ import ( commonpb "github.com/code-payments/ocp-protobuf-api/generated/go/common/v1" transactionpb "github.com/code-payments/ocp-protobuf-api/generated/go/transaction/v1" + "github.com/code-payments/ocp-server/coinbase" "github.com/code-payments/ocp-server/grpc/client" "github.com/code-payments/ocp-server/ocp/balance" "github.com/code-payments/ocp-server/ocp/common" @@ -235,6 +238,40 @@ func (s *transactionServer) handleReserveStatefulSwap( if !common.IsCoreMint(fromMint) { return handleStatefulSwapError(streamer, NewSwapDeniedError("source mint must be core mint")) } + case transactionpb.FundingSource_FUNDING_SOURCE_COINBASE_ONRAMP: + if !common.IsCoreMint(fromMint) { + return handleStatefulSwapError(streamer, NewSwapDeniedError("source mint must be core mint")) + } + + order, err := s.coinbaseClient.GetOrder(ctx, initiateReserveSwapReq.FundingId) + if err == coinbase.ErrOrderNotFound { + return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order not found")) + } else if err != nil { + log.With(zap.Error(err)).Warn("failure getting coinbase order") + return handleStatefulSwapError(streamer, err) + } + if order.Status == coinbase.OrderStatusFailed { + return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order is in a failed state")) + } + + if !strings.EqualFold(order.PurchaseAmount.Currency, common.CoreMintSymbol) { + return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order is not for the core mint")) + } + if order.PartnerUserRef != owner.PublicKey().ToBase58() { + return handleStatefulSwapError(streamer, NewSwapDeniedError("coinbase order partner user ref does not match owner")) + } + if order.DestinationAddress != sourceTimelockAccountRecord.SwapPdaAddress { + return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order destination address is not the owner's swap pda")) + } + + orderQuarks, err := decimalToQuarks(order.PurchaseAmount.Value, common.CoreMintDecimals) + if err != nil { + log.With(zap.Error(err)).Warn("invalid coinbase order purchase amount") + return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order purchase amount is invalid")) + } + if orderQuarks != initiateReserveSwapReq.SwapAmount+initiateReserveSwapReq.FeeAmount { + return handleStatefulSwapError(streamer, NewSwapDeniedError("coinbase order purchase amount does not match swap amount")) + } default: return handleStatefulSwapError(streamer, NewSwapDeniedErrorf("funding source %s is not supported", initiateReserveSwapReq.FundingSource)) } @@ -555,7 +592,7 @@ func (s *transactionServer) handleReserveStatefulSwap( switch initiateReserveSwapReq.FundingSource { case transactionpb.FundingSource_FUNDING_SOURCE_SUBMIT_INTENT: initialState = swap.StateCreated - case transactionpb.FundingSource_FUNDING_SOURCE_EXTERNAL_WALLET: + case transactionpb.FundingSource_FUNDING_SOURCE_EXTERNAL_WALLET, transactionpb.FundingSource_FUNDING_SOURCE_COINBASE_ONRAMP: initialState = swap.StateFunding default: return handleStatefulSwapError(streamer, NewSwapDeniedErrorf("funding source %s is not supported", initiateReserveSwapReq.FundingSource)) @@ -1195,3 +1232,23 @@ func toProtoSwap(record *swap.Record) (*transactionpb.SwapMetadata, error) { Signature: &commonpb.Signature{Value: decodedSignature}, }, nil } + +func decimalToQuarks(value string, decimals int) (uint64, error) { + rat, ok := new(big.Rat).SetString(value) + if !ok { + return 0, errors.Errorf("invalid decimal value: %s", value) + } + if rat.Sign() < 0 { + return 0, errors.New("amount is negative") + } + multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil) + scaled := new(big.Rat).Mul(rat, new(big.Rat).SetInt(multiplier)) + if !scaled.IsInt() { + return 0, errors.New("amount has more precision than mint decimals") + } + quarks := scaled.Num() + if !quarks.IsUint64() { + return 0, errors.New("amount overflows uint64") + } + return quarks.Uint64(), nil +} diff --git a/ocp/worker/swap/config.go b/ocp/worker/swap/config.go index 9dcf2fd..a0db090 100644 --- a/ocp/worker/swap/config.go +++ b/ocp/worker/swap/config.go @@ -18,12 +18,16 @@ const ( ExternalWalletFinalizationTimeoutConfigEnvName = envConfigPrefix + "EXTERNAL_WALLET_FINALIZATION_TIMEOUT" defaultExternalWalletFinalizationTimeout = 30 * time.Second + + CoinbaseOnrampFinalizationTimeoutConfigEnvName = envConfigPrefix + "COINBASE_ONRAMP_FINALIZATION_TIMEOUT" + defaultCoinbaseOnrampFinalizationTimeout = 15 * time.Minute ) type conf struct { batchSize config.Uint64 clientTimeoutToFund config.Duration externalWalletFinalizationTimeout config.Duration + coinbaseOnrampFinalizationTimeout config.Duration } // ConfigProvider defines how config values are pulled @@ -36,6 +40,7 @@ func WithEnvConfigs() ConfigProvider { batchSize: env.NewUint64Config(BatchSizeConfigEnvName, defaultBatchSize), clientTimeoutToFund: env.NewDurationConfig(ClientTimeoutToFundConfigEnvName, defaultClientTimeoutToFund), externalWalletFinalizationTimeout: env.NewDurationConfig(ExternalWalletFinalizationTimeoutConfigEnvName, defaultExternalWalletFinalizationTimeout), + coinbaseOnrampFinalizationTimeout: env.NewDurationConfig(CoinbaseOnrampFinalizationTimeoutConfigEnvName, defaultCoinbaseOnrampFinalizationTimeout), } } } diff --git a/ocp/worker/swap/runtime.go b/ocp/worker/swap/runtime.go index b19462c..3e2fd91 100644 --- a/ocp/worker/swap/runtime.go +++ b/ocp/worker/swap/runtime.go @@ -9,6 +9,7 @@ import ( indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" + "github.com/code-payments/ocp-server/coinbase" ocp_data "github.com/code-payments/ocp-server/ocp/data" "github.com/code-payments/ocp-server/ocp/data/nonce" "github.com/code-payments/ocp-server/ocp/data/swap" @@ -24,6 +25,7 @@ type runtime struct { vmIndexerClient indexerpb.IndexerClient integration integration.Swap solanaNoncePool *transaction.LocalNoncePool + coinbaseClient *coinbase.Client } func New( @@ -32,6 +34,7 @@ func New( vmIndexerClient indexerpb.IndexerClient, integration integration.Swap, solanaNoncePool *transaction.LocalNoncePool, + coinbaseClient *coinbase.Client, configProvider ConfigProvider, ) (worker.Runtime, error) { if err := solanaNoncePool.Validate(nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeOnDemandTransaction); err != nil { @@ -45,6 +48,7 @@ func New( vmIndexerClient: vmIndexerClient, integration: integration, solanaNoncePool: solanaNoncePool, + coinbaseClient: coinbaseClient, }, nil } diff --git a/ocp/worker/swap/util.go b/ocp/worker/swap/util.go index e32f452..af7dd40 100644 --- a/ocp/worker/swap/util.go +++ b/ocp/worker/swap/util.go @@ -423,7 +423,7 @@ func (p *runtime) buildRefundRecordsForCancelledSwap(ctx context.Context, swapRe nativeAmount = fundingIntentRecord.SendPublicPaymentMetadata.NativeAmount usdMarketValue = fundingIntentRecord.SendPublicPaymentMetadata.UsdMarketValue isReturned = true - case swap.FundingSourceExternalWallet: + case swap.FundingSourceExternalWallet, swap.FundingSourceCoinbaseOnramp: if !common.IsCoreMint(fromMint) { return nil, nil, errors.New("unexpected source mint") } @@ -621,7 +621,7 @@ func (p *runtime) maybeUpdateBalancesForFinalizedReserveSwap(ctx context.Context return 0, false, err } } - case swap.FundingSourceExternalWallet: + case swap.FundingSourceExternalWallet, swap.FundingSourceCoinbaseOnramp: if !common.IsCoreMint(fromMint) { return 0, false, errors.New("unexpected source mint") } @@ -781,7 +781,7 @@ func (p *runtime) notifySwapFinalized(ctx context.Context, swapRecord *swap.Reco currencyCode = fundingIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency nativeAmount = fundingIntentRecord.SendPublicPaymentMetadata.NativeAmount - case swap.FundingSourceExternalWallet: + case swap.FundingSourceExternalWallet, swap.FundingSourceCoinbaseOnramp: if !common.IsCoreMint(fromMint) { return errors.New("unexpected source mint") } @@ -945,6 +945,55 @@ func (p *runtime) validateExternalWalletFunding(ctx context.Context, record *swa return true, nil } +func (p *runtime) validateCoinbaseOnrampFunding(ctx context.Context, record *swap.Record) (bool, error) { + if record.FundingSource != swap.FundingSourceCoinbaseOnramp { + return false, errors.New("invalid funding source") + } + + owner, err := common.NewAccountFromPublicKeyString(record.Owner) + if err != nil { + return false, errors.Wrap(err, "error parsing owner") + } + + fromMint, err := common.NewAccountFromPublicKeyString(record.FromMint) + if err != nil { + return false, errors.Wrap(err, "error parsing from mint") + } + + sourceVmConfig, err := common.GetVmConfigForMint(ctx, p.data, fromMint) + if err != nil { + return false, errors.Wrap(err, "error getting vm config for source mint") + } + + swapAta, err := owner.ToVmSwapAta(sourceVmConfig) + if err != nil { + return false, errors.Wrap(err, "error getting swap ata") + } + + order, err := p.coinbaseClient.GetOrder(ctx, record.FundingId) + if err != nil { + return false, errors.Wrap(err, "error getting coinbase order") + } + if order.TxHash == "" { + return false, errors.New("coinbase order has no on-chain transaction") + } + + tokenBalances, err := p.data.GetBlockchainTransactionTokenBalances(ctx, order.TxHash) + if err != nil { + return false, errors.Wrap(err, "error getting token balances") + } + + deltaQuarks, err := transaction_util.GetDeltaQuarksFromTokenBalances(swapAta, tokenBalances) + if err != nil { + return false, errors.Wrap(err, "error getting delta quarks from token balances") + } + + if deltaQuarks < int64(record.SwapAmount+record.FeeAmount) { + return false, nil + } + return true, nil +} + func (p *runtime) ensureSwapDestinationIsInitialized(ctx context.Context, record *swap.Record) error { if record.Kind != swap.KindReserve { return nil diff --git a/ocp/worker/swap/worker.go b/ocp/worker/swap/worker.go index ca47758..f7137aa 100644 --- a/ocp/worker/swap/worker.go +++ b/ocp/worker/swap/worker.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" + "github.com/code-payments/ocp-server/coinbase" "github.com/code-payments/ocp-server/database/query" "github.com/code-payments/ocp-server/metrics" "github.com/code-payments/ocp-server/ocp/data/intent" @@ -156,6 +157,40 @@ func (p *runtime) handleStateFunding(ctx context.Context, record *swap.Record) e return p.markSwapCancelled(ctx, record, nil) } + return nil + case swap.FundingSourceCoinbaseOnramp: + // Look up the Coinbase order. The funding ID is the Coinbase order ID, + // and the order's TxHash holds the on-chain settlement signature once + // Coinbase has broadcast the transaction. + order, err := p.coinbaseClient.GetOrder(ctx, record.FundingId) + if err != nil { + return errors.Wrap(err, "error getting coinbase order") + } + + if order.Status == coinbase.OrderStatusFailed { + return p.markSwapCancelled(ctx, record, nil) + } + + if order.TxHash != "" { + finalizedTxn, err := p.data.GetBlockchainTransaction(ctx, order.TxHash, solana.CommitmentFinalized) + if err != nil && err != solana.ErrSignatureNotFound { + return errors.Wrap(err, "error getting finalized coinbase funding transaction") + } + + if finalizedTxn != nil { + if finalizedTxn.Err != nil || finalizedTxn.Meta.Err != nil { + return p.markSwapCancelled(ctx, record, nil) + } + return p.markSwapFunded(ctx, record) + } + } + + // Cancel the swap if the Coinbase onramp funding hasn't been finalized + // within a reasonable amount of time + if time.Since(record.CreatedAt) > p.conf.coinbaseOnrampFinalizationTimeout.Get(ctx) { + return p.markSwapCancelled(ctx, record, nil) + } + return nil default: return errors.New("unsupported funding source") @@ -180,6 +215,11 @@ func (p *runtime) handleStateFunded(ctx context.Context, record *swap.Record) er if err != nil { return errors.Wrap(err, "error validating external wallet funding") } + case swap.FundingSourceCoinbaseOnramp: + isValid, err = p.validateCoinbaseOnrampFunding(ctx, record) + if err != nil { + return errors.Wrap(err, "error validating coinbase onramp funding") + } default: return errors.New("unsupported funding source") } From fb0d9b5dd887e33f8ed39c128127d2d3e850b42d Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 11 May 2026 08:27:12 -0400 Subject: [PATCH 2/6] Pull ocp-protobuf-api v1.11.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 320aae0..21b9269 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( filippo.io/edwards25519 v1.1.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/code-payments/code-vm-indexer v1.2.0 - github.com/code-payments/ocp-protobuf-api v1.10.1-0.20260429180655-b59b2a1ebfc1 + github.com/code-payments/ocp-protobuf-api v1.11.0 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.2.1 github.com/golang/protobuf v1.5.4 diff --git a/go.sum b/go.sum index b7fe773..0d64aeb 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-vm-indexer v1.2.0 h1:rSHpBMiT9BKgmKcXg/VIoi/h0t7jNxGx07Qz59m+6Q0= github.com/code-payments/code-vm-indexer v1.2.0/go.mod h1:vn91YN2qNqb+gGJeZe2+l+TNxVmEEiRHXXnIn2Y40h8= -github.com/code-payments/ocp-protobuf-api v1.10.1-0.20260429180655-b59b2a1ebfc1 h1:zBF2FPaIwuKP6wPDw/rciXTvzCxZm0ELLcUyFYJ1fCw= -github.com/code-payments/ocp-protobuf-api v1.10.1-0.20260429180655-b59b2a1ebfc1/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= +github.com/code-payments/ocp-protobuf-api v1.11.0 h1:bvAtcOC3llKWckLKcuK2/i1aY6LorVZebWUybNG43PM= +github.com/code-payments/ocp-protobuf-api v1.11.0/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= From 00e0a8eff7b179697309189466168c38172a737a Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 11 May 2026 12:42:48 -0400 Subject: [PATCH 3/6] Drop defaultCoinbaseOnrampFinalizationTimeout to 5 minutes --- ocp/worker/swap/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocp/worker/swap/config.go b/ocp/worker/swap/config.go index a0db090..153722d 100644 --- a/ocp/worker/swap/config.go +++ b/ocp/worker/swap/config.go @@ -20,7 +20,7 @@ const ( defaultExternalWalletFinalizationTimeout = 30 * time.Second CoinbaseOnrampFinalizationTimeoutConfigEnvName = envConfigPrefix + "COINBASE_ONRAMP_FINALIZATION_TIMEOUT" - defaultCoinbaseOnrampFinalizationTimeout = 15 * time.Minute + defaultCoinbaseOnrampFinalizationTimeout = 5 * time.Minute ) type conf struct { From 04ec13b98bd252a1a54c0e96c4ab54ad0604b187 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 11 May 2026 14:12:56 -0400 Subject: [PATCH 4/6] Add more Coinbase validation in StatefulSwap --- coinbase/order.go | 4 ++++ ocp/rpc/transaction/swap.go | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/coinbase/order.go b/coinbase/order.go index f2e5a03..57ac294 100644 --- a/coinbase/order.go +++ b/coinbase/order.go @@ -34,6 +34,10 @@ const ( FeeTypeExchange FeeType = "FEE_TYPE_EXCHANGE" ) +const ( + NetworkSolana = "solana" +) + // Order is a Coinbase Onramp order as returned by the v2 API. type Order struct { OrderID string // UUID assigned by Coinbase diff --git a/ocp/rpc/transaction/swap.go b/ocp/rpc/transaction/swap.go index c80a4e0..6f107f2 100644 --- a/ocp/rpc/transaction/swap.go +++ b/ocp/rpc/transaction/swap.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/mr-tron/base58/base58" "github.com/pkg/errors" "go.uber.org/zap" @@ -243,6 +244,10 @@ func (s *transactionServer) handleReserveStatefulSwap( return handleStatefulSwapError(streamer, NewSwapDeniedError("source mint must be core mint")) } + if _, err := uuid.Parse(initiateReserveSwapReq.FundingId); err != nil { + return handleStatefulSwapError(streamer, NewSwapValidationError("funding id is not a uuid")) + } + order, err := s.coinbaseClient.GetOrder(ctx, initiateReserveSwapReq.FundingId) if err == coinbase.ErrOrderNotFound { return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order not found")) @@ -254,6 +259,9 @@ func (s *transactionServer) handleReserveStatefulSwap( return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order is in a failed state")) } + if !strings.EqualFold(order.DestinationNetwork, coinbase.NetworkSolana) { + return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order destination network is not solana")) + } if !strings.EqualFold(order.PurchaseAmount.Currency, common.CoreMintSymbol) { return handleStatefulSwapError(streamer, NewSwapValidationError("coinbase order is not for the core mint")) } From 5ef672034726f9a8d6dfa01d01b107dc01b8a66f Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 11 May 2026 14:34:09 -0400 Subject: [PATCH 5/6] Improve Coinbase order handling in swap worker --- ocp/worker/swap/config.go | 8 +++---- ocp/worker/swap/worker.go | 49 +++++++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/ocp/worker/swap/config.go b/ocp/worker/swap/config.go index 153722d..22a82bd 100644 --- a/ocp/worker/swap/config.go +++ b/ocp/worker/swap/config.go @@ -19,15 +19,15 @@ const ( ExternalWalletFinalizationTimeoutConfigEnvName = envConfigPrefix + "EXTERNAL_WALLET_FINALIZATION_TIMEOUT" defaultExternalWalletFinalizationTimeout = 30 * time.Second - CoinbaseOnrampFinalizationTimeoutConfigEnvName = envConfigPrefix + "COINBASE_ONRAMP_FINALIZATION_TIMEOUT" - defaultCoinbaseOnrampFinalizationTimeout = 5 * time.Minute + CoinbaseOnrampOrderTimeoutConfigEnvName = envConfigPrefix + "COINBASE_ONRAMP_ORDER_TIMEOUT" + defaultCoinbaseOnrampOrderTimeout = 5 * time.Minute ) type conf struct { batchSize config.Uint64 clientTimeoutToFund config.Duration externalWalletFinalizationTimeout config.Duration - coinbaseOnrampFinalizationTimeout config.Duration + coinbaseOnrampOrderTimeout config.Duration } // ConfigProvider defines how config values are pulled @@ -40,7 +40,7 @@ func WithEnvConfigs() ConfigProvider { batchSize: env.NewUint64Config(BatchSizeConfigEnvName, defaultBatchSize), clientTimeoutToFund: env.NewDurationConfig(ClientTimeoutToFundConfigEnvName, defaultClientTimeoutToFund), externalWalletFinalizationTimeout: env.NewDurationConfig(ExternalWalletFinalizationTimeoutConfigEnvName, defaultExternalWalletFinalizationTimeout), - coinbaseOnrampFinalizationTimeout: env.NewDurationConfig(CoinbaseOnrampFinalizationTimeoutConfigEnvName, defaultCoinbaseOnrampFinalizationTimeout), + coinbaseOnrampOrderTimeout: env.NewDurationConfig(CoinbaseOnrampOrderTimeoutConfigEnvName, defaultCoinbaseOnrampOrderTimeout), } } } diff --git a/ocp/worker/swap/worker.go b/ocp/worker/swap/worker.go index f7137aa..de40c8d 100644 --- a/ocp/worker/swap/worker.go +++ b/ocp/worker/swap/worker.go @@ -116,6 +116,13 @@ func (p *runtime) handleStateCreated(ctx context.Context, record *swap.Record) e } func (p *runtime) handleStateFunding(ctx context.Context, record *swap.Record) error { + log := p.log.With( + zap.String("method", "handleStateFunding"), + zap.String("swap_id", record.SwapId), + zap.String("funding_id", record.FundingId), + zap.String("owner", record.Owner), + ) + if err := p.validateSwapState(record, swap.StateFunding); err != nil { return err } @@ -167,28 +174,36 @@ func (p *runtime) handleStateFunding(ctx context.Context, record *swap.Record) e return errors.Wrap(err, "error getting coinbase order") } - if order.Status == coinbase.OrderStatusFailed { - return p.markSwapCancelled(ctx, record, nil) - } - - if order.TxHash != "" { - finalizedTxn, err := p.data.GetBlockchainTransaction(ctx, order.TxHash, solana.CommitmentFinalized) - if err != nil && err != solana.ErrSignatureNotFound { - return errors.Wrap(err, "error getting finalized coinbase funding transaction") - } + switch order.Status { + case coinbase.OrderStatusProcessing, coinbase.OrderStatusCompleted: + if order.TxHash != "" { + finalizedTxn, err := p.data.GetBlockchainTransaction(ctx, order.TxHash, solana.CommitmentFinalized) + if err != nil && err != solana.ErrSignatureNotFound { + return errors.Wrap(err, "error getting finalized coinbase funding transaction") + } - if finalizedTxn != nil { - if finalizedTxn.Err != nil || finalizedTxn.Meta.Err != nil { - return p.markSwapCancelled(ctx, record, nil) + if finalizedTxn != nil { + if finalizedTxn.Err != nil || finalizedTxn.Meta.Err != nil { + return p.markSwapCancelled(ctx, record, nil) + } + return p.markSwapFunded(ctx, record) } - return p.markSwapFunded(ctx, record) } - } - // Cancel the swap if the Coinbase onramp funding hasn't been finalized - // within a reasonable amount of time - if time.Since(record.CreatedAt) > p.conf.coinbaseOnrampFinalizationTimeout.Get(ctx) { + if time.Since(record.CreatedAt) > 2*p.conf.coinbaseOnrampOrderTimeout.Get(ctx) { + log.With( + zap.String("txn", order.TxHash), + zap.String("order_status", string(order.Status)), + ).Info("funding transaction for coinbase order is not finalizing") + } + case coinbase.OrderStatusFailed: return p.markSwapCancelled(ctx, record, nil) + default: + // Cancel the swap if the Coinbase onramp funding hasn't been finalized + // within a reasonable amount of time + if time.Since(record.CreatedAt) > p.conf.coinbaseOnrampOrderTimeout.Get(ctx) { + return p.markSwapCancelled(ctx, record, nil) + } } return nil From f303ef12deb784fca816aee5c222374b3a908221 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 11 May 2026 14:49:46 -0400 Subject: [PATCH 6/6] Update comment --- ocp/worker/swap/worker.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ocp/worker/swap/worker.go b/ocp/worker/swap/worker.go index de40c8d..bc694df 100644 --- a/ocp/worker/swap/worker.go +++ b/ocp/worker/swap/worker.go @@ -199,8 +199,9 @@ func (p *runtime) handleStateFunding(ctx context.Context, record *swap.Record) e case coinbase.OrderStatusFailed: return p.markSwapCancelled(ctx, record, nil) default: - // Cancel the swap if the Coinbase onramp funding hasn't been finalized - // within a reasonable amount of time + // Cancel the swap if the Coinbase onramp order hasn't been completed + // within a reasonable amount of time. Timeout should be greater than + // that enforced on client to avoid lost funds. if time.Since(record.CreatedAt) > p.conf.coinbaseOnrampOrderTimeout.Get(ctx) { return p.markSwapCancelled(ctx, record, nil) }