Skip to content

Commit 1f48e26

Browse files
committed
Initial implementation of the Swap RPC
1 parent a7b37b6 commit 1f48e26

File tree

12 files changed

+1107
-31
lines changed

12 files changed

+1107
-31
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
filippo.io/edwards25519 v1.1.0
77
github.com/aws/aws-sdk-go-v2 v0.17.0
88
github.com/bits-and-blooms/bloom/v3 v3.1.0
9-
github.com/code-payments/code-protobuf-api v1.19.1-0.20251112150441-9cefdedce1ef
9+
github.com/code-payments/code-protobuf-api v1.19.1-0.20251118163018-a7ac2495fb53
1010
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba
1111
github.com/emirpasic/gods v1.12.0
1212
github.com/envoyproxy/protoc-gen-validate v1.2.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
8080
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
8181
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
8282
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
83-
github.com/code-payments/code-protobuf-api v1.19.1-0.20251112150441-9cefdedce1ef h1:hl86qXa6P5HQNyJj3FXOqg5tN951W/vZ5qIrD0sg7gU=
84-
github.com/code-payments/code-protobuf-api v1.19.1-0.20251112150441-9cefdedce1ef/go.mod h1:fl4xu32MeNpGZR3wFhwEeKO0qVDuxYBNkqnvuADt6cA=
83+
github.com/code-payments/code-protobuf-api v1.19.1-0.20251118163018-a7ac2495fb53 h1:+a1md62yEAg55n7P9qSCwRaeAqOQiDOTL95O/oHMdis=
84+
github.com/code-payments/code-protobuf-api v1.19.1-0.20251118163018-a7ac2495fb53/go.mod h1:fl4xu32MeNpGZR3wFhwEeKO0qVDuxYBNkqnvuADt6cA=
8585
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba h1:Bkp+gmeb6Y2PWXfkSCTMBGWkb2P1BujRDSjWeI+0j5I=
8686
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba/go.mod h1:jSiifpiBpyBQ8q2R0MGEbkSgWC6sbdRTyDBntmW+j1E=
8787
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw=

pkg/code/antispam/guard.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,17 @@ func (g *Guard) AllowDistribution(ctx context.Context, owner *common.Account, is
8686
}
8787
return allow, nil
8888
}
89+
90+
func (g *Guard) AllowSwap(ctx context.Context, owner, fromMint, toMint *common.Account) (bool, error) {
91+
tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowSwap")
92+
defer tracer.End()
93+
94+
allow, reason, err := g.integration.AllowSwap(ctx, owner, fromMint, toMint)
95+
if err != nil {
96+
return false, err
97+
}
98+
if !allow {
99+
recordDenialEvent(ctx, actionSwap, reason)
100+
}
101+
return allow, nil
102+
}

pkg/code/antispam/integration.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ type Integration interface {
2020
AllowReceivePayments(ctx context.Context, owner *common.Account, isPublic bool) (bool, string, error)
2121

2222
AllowDistribution(ctx context.Context, owner *common.Account, isPublic bool) (bool, string, error)
23+
24+
AllowSwap(ctx context.Context, owner, fromMint, toMint *common.Account) (bool, string, error)
2325
}
2426

2527
type allowEverythingIntegration struct {
@@ -49,3 +51,7 @@ func (i *allowEverythingIntegration) AllowReceivePayments(ctx context.Context, o
4951
func (i *allowEverythingIntegration) AllowDistribution(ctx context.Context, owner *common.Account, isPublic bool) (bool, string, error) {
5052
return true, "", nil
5153
}
54+
55+
func (i *allowEverythingIntegration) AllowSwap(ctx context.Context, owner, fromMint, toMint *common.Account) (bool, string, error) {
56+
return true, "", nil
57+
}

pkg/code/antispam/metrics.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const (
1616
actionReceivePayments = "ReceivePayments"
1717
actionDistribution = "Distribution"
1818

19+
actionSwap = "Swap"
20+
1921
actionWelcomeBonus = "WelcomeBonus"
2022
)
2123

pkg/code/async/geyser/external_deposit.go

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func initiateExternalDepositIntoVm(ctx context.Context, data code_data.Provider,
108108
var memoryIndex uint16
109109
_, err = retry.Retry(
110110
func() error {
111-
memoryAccount, memoryIndex, err = getVirtualTimelockAccountLocationInMemory(ctx, vmIndexerClient, vmConfig.Vm, userAuthority)
111+
memoryAccount, memoryIndex, err = common.GetVirtualTimelockAccountLocationInMemory(ctx, vmIndexerClient, vmConfig.Vm, userAuthority)
112112
return err
113113
},
114114
waitForFinalizationRetryStrategies...,
@@ -394,31 +394,6 @@ func processPotentialExternalDepositIntoVm(ctx context.Context, data code_data.P
394394
}
395395
}
396396

397-
func getVirtualTimelockAccountLocationInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, owner *common.Account) (*common.Account, uint16, error) {
398-
resp, err := vmIndexerClient.GetVirtualTimelockAccounts(ctx, &indexerpb.GetVirtualTimelockAccountsRequest{
399-
VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()},
400-
Owner: &indexerpb.Address{Value: owner.PublicKey().ToBytes()},
401-
})
402-
if err != nil {
403-
return nil, 0, err
404-
} else if resp.Result != indexerpb.GetVirtualTimelockAccountsResponse_OK {
405-
return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String())
406-
}
407-
408-
if len(resp.Items) > 1 {
409-
return nil, 0, errors.New("multiple results returned")
410-
} else if resp.Items[0].Storage.GetMemory() == nil {
411-
return nil, 0, errors.New("account is compressed or hasn't been initialized")
412-
}
413-
414-
protoMemory := resp.Items[0].Storage.GetMemory()
415-
memory, err := common.NewAccountFromPublicKeyBytes(protoMemory.Account.Value)
416-
if err != nil {
417-
return nil, 0, err
418-
}
419-
return memory, uint16(protoMemory.Index), nil
420-
}
421-
422397
func getDeltaQuarksFromTokenBalances(tokenAccount *common.Account, tokenBalances *solana.TransactionTokenBalances) (int64, error) {
423398
var preQuarkBalance, postQuarkBalance int64
424399
var err error

pkg/code/common/vm.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package common
33
import (
44
"context"
55

6+
"github.com/pkg/errors"
7+
8+
indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1"
9+
610
"github.com/code-payments/code-server/pkg/code/config"
711
code_data "github.com/code-payments/code-server/pkg/code/data"
812
)
@@ -103,3 +107,28 @@ func GetVmConfigForMint(ctx context.Context, data code_data.Provider, mint *Acco
103107
return nil, ErrUnsupportedMint
104108
}
105109
}
110+
111+
func GetVirtualTimelockAccountLocationInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, owner *Account) (*Account, uint16, error) {
112+
resp, err := vmIndexerClient.GetVirtualTimelockAccounts(ctx, &indexerpb.GetVirtualTimelockAccountsRequest{
113+
VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()},
114+
Owner: &indexerpb.Address{Value: owner.PublicKey().ToBytes()},
115+
})
116+
if err != nil {
117+
return nil, 0, err
118+
} else if resp.Result != indexerpb.GetVirtualTimelockAccountsResponse_OK {
119+
return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String())
120+
}
121+
122+
if len(resp.Items) > 1 {
123+
return nil, 0, errors.New("multiple results returned")
124+
} else if resp.Items[0].Storage.GetMemory() == nil {
125+
return nil, 0, errors.New("account is compressed or hasn't been initialized")
126+
}
127+
128+
protoMemory := resp.Items[0].Storage.GetMemory()
129+
memory, err := NewAccountFromPublicKeyBytes(protoMemory.Account.Value)
130+
if err != nil {
131+
return nil, 0, err
132+
}
133+
return memory, uint16(protoMemory.Index), nil
134+
}

pkg/code/server/transaction/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const (
1818
SubmitIntentTimeoutConfigEnvName = envConfigPrefix + "SUBMIT_INTENT_TIMEOUT"
1919
defaultSubmitIntentTimeout = 5 * time.Second
2020

21+
SwapTimeoutConfigEnvName = envConfigPrefix + "SWAP_TIMEOUT"
22+
defaultSwapTimeout = 60 * time.Second
23+
2124
ClientReceiveTimeoutConfigEnvName = envConfigPrefix + "CLIENT_RECEIVE_TIMEOUT"
2225
defaultClientReceiveTimeout = time.Second
2326

@@ -43,6 +46,7 @@ type conf struct {
4346
disableAmlChecks config.Bool // To avoid limits during testing
4447
disableBlockchainChecks config.Bool // To avoid blockchain checks during testing
4548
submitIntentTimeout config.Duration
49+
swapTimeout config.Duration
4650
clientReceiveTimeout config.Duration
4751
feeCollectorOwnerPublicKey config.String
4852
createOnSendWithdrawalUsdFee config.Float64
@@ -63,6 +67,7 @@ func WithEnvConfigs() ConfigProvider {
6367
disableAmlChecks: wrapper.NewBoolConfig(memory.NewConfig(false), false),
6468
disableBlockchainChecks: wrapper.NewBoolConfig(memory.NewConfig(false), false),
6569
submitIntentTimeout: env.NewDurationConfig(SubmitIntentTimeoutConfigEnvName, defaultSubmitIntentTimeout),
70+
swapTimeout: env.NewDurationConfig(SwapTimeoutConfigEnvName, defaultSwapTimeout),
6671
clientReceiveTimeout: env.NewDurationConfig(ClientReceiveTimeoutConfigEnvName, defaultClientReceiveTimeout),
6772
feeCollectorOwnerPublicKey: env.NewStringConfig(FeeCollectorOwnerPublicKeyConfigEnvName, defaultFeeCollectorPublicKey),
6873
createOnSendWithdrawalUsdFee: env.NewFloat64Config(CreateOnSendWithdrawalUsdFeeConfigEnvName, defaultCreateOnSendWithdrawalUsdFee),
@@ -90,6 +95,7 @@ func withManualTestOverrides(overrides *testOverrides) ConfigProvider {
9095
disableAmlChecks: wrapper.NewBoolConfig(memory.NewConfig(!overrides.enableAmlChecks), false),
9196
disableBlockchainChecks: wrapper.NewBoolConfig(memory.NewConfig(true), true),
9297
submitIntentTimeout: wrapper.NewDurationConfig(memory.NewConfig(defaultSubmitIntentTimeout), defaultSubmitIntentTimeout),
98+
swapTimeout: wrapper.NewDurationConfig(memory.NewConfig(defaultSwapTimeout), defaultSwapTimeout),
9399
clientReceiveTimeout: wrapper.NewDurationConfig(memory.NewConfig(overrides.clientReceiveTimeout), defaultClientReceiveTimeout),
94100
feeCollectorOwnerPublicKey: wrapper.NewStringConfig(memory.NewConfig(overrides.feeCollectorOwnerPublicKey), defaultFeeCollectorPublicKey),
95101
createOnSendWithdrawalUsdFee: wrapper.NewFloat64Config(memory.NewConfig(defaultCreateOnSendWithdrawalUsdFee), defaultCreateOnSendWithdrawalUsdFee),

pkg/code/server/transaction/errors.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,42 @@ func (e IntentDeniedError) Error() string {
7777
return e.message
7878
}
7979

80+
type SwapValidationError struct {
81+
message string
82+
}
83+
84+
func NewSwapValidationError(message string) SwapValidationError {
85+
return SwapValidationError{
86+
message: message,
87+
}
88+
}
89+
90+
func NewSwapValidationErrorf(format string, args ...any) SwapValidationError {
91+
return NewSwapValidationError(fmt.Sprintf(format, args...))
92+
}
93+
94+
func (e SwapValidationError) Error() string {
95+
return e.message
96+
}
97+
98+
type SwapDeniedError struct {
99+
message string
100+
}
101+
102+
func NewSwapDeniedError(message string) SwapDeniedError {
103+
return SwapDeniedError{
104+
message: message,
105+
}
106+
}
107+
108+
func NewSwapDeniedErrorf(format string, args ...any) SwapDeniedError {
109+
return NewSwapDeniedError(fmt.Sprintf(format, args...))
110+
}
111+
112+
func (e SwapDeniedError) Error() string {
113+
return e.message
114+
}
115+
80116
type StaleStateError struct {
81117
message string
82118
}
@@ -251,6 +287,61 @@ func handleSubmitIntentStructuredError(streamer transactionpb.Transaction_Submit
251287
return streamer.Send(errResp)
252288
}
253289

290+
func handleSwapError(streamer transactionpb.Transaction_SwapServer, err error) error {
291+
// gRPC status errors are passed through as is
292+
if _, ok := status.FromError(err); ok {
293+
return err
294+
}
295+
296+
// Case 1: Errors that map to a Code error response
297+
switch err.(type) {
298+
case SwapValidationError:
299+
return handleSwapStructuredError(
300+
streamer,
301+
transactionpb.SwapResponse_Error_INVALID_SWAP,
302+
toReasonStringErrorDetails(err),
303+
)
304+
case SwapDeniedError:
305+
return handleSwapStructuredError(
306+
streamer,
307+
transactionpb.SwapResponse_Error_DENIED,
308+
toDeniedErrorDetails(err),
309+
)
310+
}
311+
312+
switch err {
313+
case ErrInvalidSignature:
314+
return handleSwapStructuredError(
315+
streamer,
316+
transactionpb.SwapResponse_Error_SIGNATURE_ERROR,
317+
toReasonStringErrorDetails(err),
318+
)
319+
case ErrNotImplemented:
320+
return status.Error(codes.Unimplemented, err.Error())
321+
}
322+
323+
// Case 2: Errors that map to gRPC status errors
324+
switch err {
325+
case ErrTimedOutReceivingRequest, context.DeadlineExceeded:
326+
return status.Error(codes.DeadlineExceeded, err.Error())
327+
case context.Canceled:
328+
return status.Error(codes.Canceled, err.Error())
329+
}
330+
return status.Error(codes.Internal, "rpc server failure")
331+
}
332+
333+
func handleSwapStructuredError(streamer transactionpb.Transaction_SwapServer, code transactionpb.SwapResponse_Error_Code, errorDetails ...*transactionpb.ErrorDetails) error {
334+
errResp := &transactionpb.SwapResponse{
335+
Response: &transactionpb.SwapResponse_Error_{
336+
Error: &transactionpb.SwapResponse_Error{
337+
Code: code,
338+
ErrorDetails: errorDetails,
339+
},
340+
},
341+
}
342+
return streamer.Send(errResp)
343+
}
344+
254345
func shouldFilterSubmitIntentFailureMetricReport(err error) bool {
255346
if statusErr, ok := status.FromError(err); ok {
256347
switch statusErr.Code() {

pkg/code/server/transaction/server.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/sirupsen/logrus"
99

1010
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
11+
indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1"
1112

1213
"github.com/code-payments/code-server/pkg/code/aml"
1314
"github.com/code-payments/code-server/pkg/code/antispam"
@@ -22,7 +23,8 @@ type transactionServer struct {
2223
log *logrus.Entry
2324
conf *conf
2425

25-
data code_data.Provider
26+
data code_data.Provider
27+
vmIndexerClient indexerpb.IndexerClient
2628

2729
auth *auth_util.RPCSignatureVerifier
2830

@@ -47,6 +49,7 @@ type transactionServer struct {
4749

4850
func NewTransactionServer(
4951
data code_data.Provider,
52+
vmIndexerClient indexerpb.IndexerClient,
5053
submitIntentIntegration SubmitIntentIntegration,
5154
airdropIntegration AirdropIntegration,
5255
antispamGuard *antispam.Guard,
@@ -72,7 +75,8 @@ func NewTransactionServer(
7275
log: logrus.StandardLogger().WithField("type", "transaction/v2/server"),
7376
conf: conf,
7477

75-
data: data,
78+
data: data,
79+
vmIndexerClient: vmIndexerClient,
7680

7781
auth: auth_util.NewRPCSignatureVerifier(data),
7882

0 commit comments

Comments
 (0)