* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+202
View File
@@ -0,0 +1,202 @@
// Package keeper implements DID integration for the DEX module
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/x/dex/types"
)
// ValidateDIDOwnership verifies that the transaction sender owns the specified DID
func (k Keeper) ValidateDIDOwnership(ctx sdk.Context, did string, sender sdk.AccAddress) error {
// Get DID document from DID keeper
didDoc, err := k.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return fmt.Errorf("failed to get DID document: %w", err)
}
if didDoc == nil {
return fmt.Errorf("DID document not found for %s", did)
}
// Verify sender is the controller of the DID
if !k.isDIDController(didDoc, sender.String()) {
return fmt.Errorf("sender %s is not the controller of DID %s", sender, did)
}
return nil
}
// isDIDController checks if an address is a controller of the DID
func (k Keeper) isDIDController(didDoc any, address string) bool {
// This is a simplified check - actual implementation would depend on DID document structure
// For now, we'll assume the DID document has a Controller field or similar
// The actual implementation should match the x/did module's structure
// TODO: Implement proper controller verification based on actual DID document structure
// This might involve checking:
// - didDoc.Controller field
// - didDoc.Authentication keys
// - didDoc.AssertionMethod keys
return true // Placeholder - always return true for now
}
// GetDIDCapabilities retrieves the DEX-related capabilities for a DID
func (k Keeper) GetDIDCapabilities(ctx sdk.Context, did string) ([]string, error) {
// Get DID document
didDoc, err := k.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return nil, fmt.Errorf("failed to get DID document: %w", err)
}
if didDoc == nil {
return nil, fmt.Errorf("DID document not found for %s", did)
}
// Extract DEX-related capabilities from the DID document
// This would typically be stored in service endpoints or custom fields
capabilities := []string{
"swap",
"liquidity",
"orders",
}
return capabilities, nil
}
// AuthenticateDIDOperation verifies that a DID is authorized for a specific DEX operation
func (k Keeper) AuthenticateDIDOperation(
ctx sdk.Context,
did string,
operation string,
params map[string]any,
) error {
// Get DID document to verify it exists and is active
didDoc, err := k.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return fmt.Errorf("failed to authenticate DID: %w", err)
}
if didDoc == nil {
return fmt.Errorf("DID %s not found", did)
}
// Check if DID has the required capability for this operation
capabilities, err := k.GetDIDCapabilities(ctx, did)
if err != nil {
return fmt.Errorf("failed to get DID capabilities: %w", err)
}
// Map operations to required capabilities
requiredCapability := k.getRequiredCapability(operation)
if !k.hasCapability(capabilities, requiredCapability) {
return fmt.Errorf("DID %s lacks capability for operation %s", did, operation)
}
// Additional authentication checks could be added here:
// - Check if DID has sufficient reputation
// - Check if DID has completed KYC/AML if required
// - Check rate limits for the DID
return nil
}
// getRequiredCapability maps DEX operations to required capabilities
func (k Keeper) getRequiredCapability(operation string) string {
switch operation {
case "swap", "execute_swap":
return "swap"
case "provide_liquidity", "remove_liquidity":
return "liquidity"
case "create_order", "cancel_order":
return "orders"
default:
return operation
}
}
// hasCapability checks if a capability exists in the list
func (k Keeper) hasCapability(capabilities []string, required string) bool {
for _, cap := range capabilities {
if cap == required {
return true
}
}
return false
}
// RecordDIDActivity records DEX activity for a DID (for analytics and compliance)
func (k Keeper) RecordDIDActivity(
ctx sdk.Context,
did string,
activity types.DEXActivity,
) error {
// Store activity record keyed by DID and timestamp
activityKey := GetDIDActivityKey(did, ctx.BlockTime().Unix())
// Store the activity
if err := k.DIDActivities.Set(ctx, activityKey, activity); err != nil {
return fmt.Errorf("failed to record DID activity: %w", err)
}
// Emit event for activity tracking
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeDIDActivity,
sdk.NewAttribute("did", did),
sdk.NewAttribute("activity_type", activity.Type),
sdk.NewAttribute("timestamp", fmt.Sprintf("%d", ctx.BlockTime().Unix())),
),
)
return nil
}
// GetDIDActivityHistory retrieves the activity history for a DID
func (k Keeper) GetDIDActivityHistory(
ctx sdk.Context,
did string,
limit uint32,
) ([]types.DEXActivity, error) {
activities := make([]types.DEXActivity, 0)
// Walk through activities for this DID
prefix := GetDIDActivityPrefix(did)
iterator, err := k.DIDActivities.Iterate(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to iterate DID activities: %w", err)
}
defer iterator.Close()
count := uint32(0)
for ; iterator.Valid() && count < limit; iterator.Next() {
key, err := iterator.Key()
if err != nil {
continue
}
// Check if key starts with the DID prefix
if len(key) >= len(prefix) && string(key[:len(prefix)]) == prefix {
activity, err := iterator.Value()
if err == nil {
activities = append(activities, activity)
count++
}
}
}
return activities, nil
}
// GetDIDActivityPrefix returns the key prefix for a DID's activities
func GetDIDActivityPrefix(did string) string {
return fmt.Sprintf("did_activity_%s_", did)
}
// GetDIDActivityKey returns the key for storing a DID activity
func GetDIDActivityKey(did string, timestamp int64) string {
return fmt.Sprintf("did_activity_%s_%d", did, timestamp)
}
+235
View File
@@ -0,0 +1,235 @@
// Package keeper implements DWN integration for the DEX module
package keeper
import (
"encoding/json"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/x/dex/types"
)
// StoreDEXAccountInDWN stores DEX account information in DWN
func (k Keeper) StoreDEXAccountInDWN(
ctx sdk.Context,
account *types.InterchainDEXAccount,
) error {
// Create DWN record
record := types.DWNRecord{
ID: fmt.Sprintf("dex_account_%s_%s", account.Did, account.ConnectionId),
DID: account.Did,
Type: "dex_account",
Data: account,
Timestamp: ctx.BlockTime(),
Metadata: map[string]string{
"connection_id": account.ConnectionId,
"port_id": account.PortId,
"status": account.Status.String(),
},
}
// Store in DWN (placeholder - actual implementation would use DWN keeper)
if err := k.storeDWNRecord(ctx, record); err != nil {
return fmt.Errorf("failed to store DEX account in DWN: %w", err)
}
return nil
}
// StoreSwapRecordInDWN stores swap transaction in DWN
func (k Keeper) StoreSwapRecordInDWN(
ctx sdk.Context,
did string,
connectionID string,
swapData map[string]any,
) error {
// Create DWN record for swap
record := types.DWNRecord{
ID: fmt.Sprintf("swap_%s_%d", did, ctx.BlockTime().Unix()),
DID: did,
Type: "dex_swap",
Data: swapData,
Timestamp: ctx.BlockTime(),
Metadata: map[string]string{
"connection_id": connectionID,
"operation": "swap",
},
}
// Store in DWN
if err := k.storeDWNRecord(ctx, record); err != nil {
return fmt.Errorf("failed to store swap record in DWN: %w", err)
}
return nil
}
// StoreLiquidityRecordInDWN stores liquidity operation in DWN
func (k Keeper) StoreLiquidityRecordInDWN(
ctx sdk.Context,
did string,
connectionID string,
operationType string, // "provide" or "remove"
liquidityData map[string]any,
) error {
// Create DWN record for liquidity operation
record := types.DWNRecord{
ID: fmt.Sprintf("liquidity_%s_%s_%d", operationType, did, ctx.BlockTime().Unix()),
DID: did,
Type: fmt.Sprintf("dex_liquidity_%s", operationType),
Data: liquidityData,
Timestamp: ctx.BlockTime(),
Metadata: map[string]string{
"connection_id": connectionID,
"operation": fmt.Sprintf("liquidity_%s", operationType),
},
}
// Store in DWN
if err := k.storeDWNRecord(ctx, record); err != nil {
return fmt.Errorf("failed to store liquidity record in DWN: %w", err)
}
return nil
}
// StoreOrderRecordInDWN stores order information in DWN
func (k Keeper) StoreOrderRecordInDWN(
ctx sdk.Context,
did string,
connectionID string,
orderID string,
orderData map[string]any,
) error {
// Create DWN record for order
record := types.DWNRecord{
ID: fmt.Sprintf("order_%s", orderID),
DID: did,
Type: "dex_order",
Data: orderData,
Timestamp: ctx.BlockTime(),
Metadata: map[string]string{
"connection_id": connectionID,
"order_id": orderID,
"operation": "order",
},
}
// Store in DWN
if err := k.storeDWNRecord(ctx, record); err != nil {
return fmt.Errorf("failed to store order record in DWN: %w", err)
}
return nil
}
// RetrieveDEXHistoryFromDWN retrieves DEX operation history from DWN
func (k Keeper) RetrieveDEXHistoryFromDWN(
ctx sdk.Context,
did string,
recordType string,
limit int,
) ([]types.DWNRecord, error) {
// Query DWN for records (placeholder implementation)
records, err := k.queryDWNRecords(ctx, did, recordType, limit)
if err != nil {
return nil, fmt.Errorf("failed to retrieve DEX history from DWN: %w", err)
}
return records, nil
}
// StorePortfolioSnapshotInDWN stores portfolio snapshot in DWN
func (k Keeper) StorePortfolioSnapshotInDWN(
ctx sdk.Context,
did string,
portfolio any,
) error {
// Create DWN record for portfolio snapshot
record := types.DWNRecord{
ID: fmt.Sprintf("portfolio_%s_%d", did, ctx.BlockTime().Unix()),
DID: did,
Type: "dex_portfolio_snapshot",
Data: portfolio,
Timestamp: ctx.BlockTime(),
Metadata: map[string]string{
"snapshot_time": ctx.BlockTime().Format(time.RFC3339),
},
}
// Store in DWN
if err := k.storeDWNRecord(ctx, record); err != nil {
return fmt.Errorf("failed to store portfolio snapshot in DWN: %w", err)
}
return nil
}
// storeDWNRecord stores a record in DWN (placeholder implementation)
func (k Keeper) storeDWNRecord(ctx sdk.Context, record types.DWNRecord) error {
// This is a placeholder implementation
// Actual implementation would use the DWN keeper interface
// Serialize record
data, err := json.Marshal(record)
if err != nil {
return fmt.Errorf("failed to serialize DWN record: %w", err)
}
// Log the operation (placeholder for actual DWN storage)
k.Logger(ctx).Info("Storing record in DWN",
"record_id", record.ID,
"did", record.DID,
"type", record.Type,
"size", len(data),
)
// TODO: Implement actual DWN storage when DWN keeper is available
// k.dwnKeeper.StoreRecord(ctx, record.DID, record.ID, data)
return nil
}
// queryDWNRecords queries records from DWN (placeholder implementation)
func (k Keeper) queryDWNRecords(
ctx sdk.Context,
did string,
recordType string,
limit int,
) ([]types.DWNRecord, error) {
// This is a placeholder implementation
// Actual implementation would use the DWN keeper interface
// Log the query
k.Logger(ctx).Info("Querying DWN records",
"did", did,
"type", recordType,
"limit", limit,
)
// TODO: Implement actual DWN query when DWN keeper is available
// records := k.dwnKeeper.QueryRecords(ctx, did, recordType, limit)
// Return empty list for now
return []types.DWNRecord{}, nil
}
// DeleteDWNRecord deletes a record from DWN
func (k Keeper) DeleteDWNRecord(
ctx sdk.Context,
did string,
recordID string,
) error {
// Log the deletion
k.Logger(ctx).Info("Deleting DWN record",
"did", did,
"record_id", recordID,
)
// TODO: Implement actual DWN deletion when DWN keeper is available
// return k.dwnKeeper.DeleteRecord(ctx, did, recordID)
return nil
}
+84
View File
@@ -0,0 +1,84 @@
package keeper
import (
"fmt"
"github.com/sonr-io/sonr/x/dex/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis initializes the module's state from a specified GenesisState
func (k Keeper) InitGenesis(ctx sdk.Context, state types.GenesisState) {
// Set params
if err := k.Params.Set(ctx, state.Params); err != nil {
panic(fmt.Sprintf("failed to set params: %v", err))
}
// Set port ID - use default if empty
portID := state.PortId
if portID == "" {
portID = types.PortID
}
// Only try to bind to port if it is not already bound
if !k.IsBound(ctx, portID) {
// Module binds to the port on InitChain
// and claims the returned capability
if err := k.BindPort(ctx, portID); err != nil {
panic(fmt.Sprintf("could not claim port capability: %v", err))
}
}
// Restore accounts
for _, account := range state.Accounts {
accountKey := GetAccountKey(account.Did, account.ConnectionId)
if err := k.Accounts.Set(ctx, accountKey, *account); err != nil {
panic(fmt.Sprintf("failed to set account: %v", err))
}
}
// Set account sequence
if err := k.AccountSequence.Set(ctx, state.AccountSequence); err != nil {
panic(fmt.Sprintf("failed to set account sequence: %v", err))
}
}
// ExportGenesis exports the module's state
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
params, err := k.Params.Get(ctx)
if err != nil {
params = types.Params{} // Use default params if not set
}
var accounts []*types.InterchainDEXAccount
err = k.Accounts.Walk(
ctx,
nil,
func(key string, value types.InterchainDEXAccount) (bool, error) {
accounts = append(accounts, &value)
return false, nil
},
)
if err != nil {
panic(fmt.Sprintf("failed to export accounts: %v", err))
}
sequence, err := k.AccountSequence.Peek(ctx)
if err != nil {
sequence = 0
}
return &types.GenesisState{
Params: params,
PortId: types.PortID,
Accounts: accounts,
AccountSequence: sequence,
}
}
// IsBound checks if the port is already bound
func (k Keeper) IsBound(ctx sdk.Context, portID string) bool {
_, ok := k.ScopedKeeper.GetCapability(ctx, fmt.Sprintf("ports/%s", portID))
return ok
}
+103
View File
@@ -0,0 +1,103 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types"
clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
connectiontypes "github.com/cosmos/ibc-go/v8/modules/core/03-connection/types"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
)
// ValidateConnection validates an IBC connection exists and is open
func (k Keeper) ValidateConnection(ctx sdk.Context, connectionID string) error {
connection, found := k.connectionKeeper.GetConnection(ctx, connectionID)
if !found {
return fmt.Errorf("connection %s not found", connectionID)
}
if connection.State != connectiontypes.OPEN {
return fmt.Errorf("connection %s is not open", connectionID)
}
return nil
}
// GetChannelCapability retrieves the channel capability
func (k Keeper) GetChannelCapability(ctx sdk.Context, portID, channelID string) (*capabilitytypes.Capability, error) {
capability, ok := k.ScopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(portID, channelID))
if !ok {
return nil, fmt.Errorf(
"capability not found for port %s channel %s: %w",
portID, channelID,
channeltypes.ErrChannelCapabilityNotFound,
)
}
return capability, nil
}
// GetChannel retrieves an IBC channel
func (k Keeper) GetChannel(ctx sdk.Context, portID, channelID string) (channeltypes.Channel, bool) {
return k.channelKeeper.GetChannel(ctx, portID, channelID)
}
// GetNextSequenceSend returns the next sequence send for a channel
func (k Keeper) GetNextSequenceSend(ctx sdk.Context, portID, channelID string) (uint64, bool) {
return k.channelKeeper.GetNextSequenceSend(ctx, portID, channelID)
}
// SendPacket sends an IBC packet
func (k Keeper) SendPacket(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
portID string,
channelID string,
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
data []byte,
) (uint64, error) {
return k.channelKeeper.SendPacket(
ctx,
chanCap,
portID,
channelID,
timeoutHeight,
timeoutTimestamp,
data,
)
}
// BindPort binds a port and claims the capability
func (k Keeper) BindPort(ctx sdk.Context, portID string) error {
capability := k.PortKeeper.BindPort(ctx, portID)
return k.ClaimCapability(ctx, capability, host.PortPath(portID))
}
// ClaimCapability claims a capability
func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error {
return k.ScopedKeeper.ClaimCapability(ctx, cap, name)
}
// AuthenticateCapability authenticates a capability
func (k Keeper) AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) bool {
return k.ScopedKeeper.AuthenticateCapability(ctx, cap, name)
}
// GetConnectionEnd retrieves an IBC connection
func (k Keeper) GetConnectionEnd(ctx sdk.Context, connectionID string) (connectiontypes.ConnectionEnd, bool) {
return k.connectionKeeper.GetConnection(ctx, connectionID)
}
// IsConnectionOpen checks if a connection is open
func (k Keeper) IsConnectionOpen(ctx sdk.Context, connectionID string) bool {
connection, found := k.GetConnectionEnd(ctx, connectionID)
return found && connection.State == connectiontypes.OPEN
}
// IsChannelOpen checks if a channel is open
func (k Keeper) IsChannelOpen(ctx sdk.Context, portID, channelID string) bool {
channel, found := k.GetChannel(ctx, portID, channelID)
return found && channel.State == channeltypes.OPEN
}
+144
View File
@@ -0,0 +1,144 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
"github.com/sonr-io/sonr/x/dex/types"
)
// OnChanOpenInit handles channel initialization for ICA
func (k Keeper) OnChanOpenInit(
ctx sdk.Context,
order channeltypes.Order,
connectionHops []string,
portID string,
channelID string,
counterparty channeltypes.Counterparty,
version string,
) error {
// Claim capability for the channel
capability := k.PortKeeper.BindPort(ctx, portID)
if err := k.ScopedKeeper.ClaimCapability(ctx, capability, channelCapabilityPath(portID, channelID)); err != nil {
return fmt.Errorf("failed to claim capability: %w", err)
}
k.Logger(ctx).Info("ICA channel initialized",
"port", portID,
"channel", channelID,
"connection", connectionHops[0],
)
return nil
}
// OnChanOpenAck handles channel acknowledgment for ICA
func (k Keeper) OnChanOpenAck(
ctx sdk.Context,
portID,
channelID string,
counterpartyChannelID string,
counterpartyVersion string,
) error {
// Parse counterparty version to get ICA address
metadata, err := parseICAMetadata(counterpartyVersion)
if err != nil {
return fmt.Errorf("failed to parse ICA metadata: %w", err)
}
// Update DEX account with ICA address
if err := k.OnICAAccountCreated(ctx, portID, metadata.Address); err != nil {
return fmt.Errorf("failed to update DEX account: %w", err)
}
k.Logger(ctx).Info("ICA channel acknowledged",
"port", portID,
"channel", channelID,
"ica_address", metadata.Address,
)
return nil
}
// OnAcknowledgementPacket handles ICA packet acknowledgments
func (k Keeper) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
acknowledgement []byte,
relayer sdk.AccAddress,
) error {
var ack channeltypes.Acknowledgement
if err := k.cdc.Unmarshal(acknowledgement, &ack); err != nil {
return fmt.Errorf("failed to unmarshal acknowledgement: %w", err)
}
// Log the acknowledgment
k.Logger(ctx).Info("ICA packet acknowledged",
"sequence", packet.Sequence,
"source_port", packet.SourcePort,
"source_channel", packet.SourceChannel,
"success", ack.Success(),
)
// Emit event for successful/failed transaction
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeICAPacketAcknowledged,
sdk.NewAttribute("sequence", fmt.Sprintf("%d", packet.Sequence)),
sdk.NewAttribute("source_port", packet.SourcePort),
sdk.NewAttribute("source_channel", packet.SourceChannel),
sdk.NewAttribute("success", fmt.Sprintf("%t", ack.Success())),
),
)
return nil
}
// OnTimeoutPacket handles ICA packet timeouts
func (k Keeper) OnTimeoutPacket(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) error {
k.Logger(ctx).Error("ICA packet timed out",
"sequence", packet.Sequence,
"source_port", packet.SourcePort,
"source_channel", packet.SourceChannel,
)
// Emit timeout event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeICAPacketTimeout,
sdk.NewAttribute("sequence", fmt.Sprintf("%d", packet.Sequence)),
sdk.NewAttribute("source_port", packet.SourcePort),
sdk.NewAttribute("source_channel", packet.SourceChannel),
),
)
return nil
}
// Helper functions
func channelCapabilityPath(portID, channelID string) string {
return fmt.Sprintf("%s/%s/%s/%s", "ports", portID, "channels", channelID)
}
// ICAMetadata represents parsed ICA metadata from version string
type ICAMetadata struct {
Address string
Version string
}
// parseICAMetadata extracts ICA address from version metadata
func parseICAMetadata(version string) (*ICAMetadata, error) {
// This is a simplified version - actual parsing depends on ICA version format
// The version string typically contains JSON with the ICA address
// For now, we'll return a placeholder
return &ICAMetadata{
Address: version, // In reality, this would be parsed from JSON
Version: "ics27-1",
}, nil
}
+248
View File
@@ -0,0 +1,248 @@
package keeper
import (
"fmt"
"time"
"github.com/sonr-io/sonr/x/dex/types"
sdk "github.com/cosmos/cosmos-sdk/types"
icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types"
host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
)
// RegisterDEXAccount registers a new ICA account for DEX operations
func (k Keeper) RegisterDEXAccount(
ctx sdk.Context,
did string,
connectionID string,
features []string,
) (*types.InterchainDEXAccount, error) {
// Validate inputs
if did == "" {
return nil, fmt.Errorf("DID cannot be empty")
}
if connectionID == "" {
return nil, fmt.Errorf("connection ID cannot be empty")
}
// Validate DID exists by trying to get the document
if _, err := k.didKeeper.GetDIDDocument(ctx, did); err != nil {
return nil, fmt.Errorf("DID %s does not exist: %w", did, err)
}
// Check if account already exists
accountKey := GetAccountKey(did, connectionID)
existing, err := k.Accounts.Get(ctx, accountKey)
if err == nil {
// Return existing account regardless of status (idempotent)
return &existing, nil
}
// Generate unique port ID
portID := GetPortID(did, connectionID)
// Register ICA account
if err := k.icaControllerKeeper.RegisterInterchainAccount(
ctx,
connectionID,
portID,
"", // Use default version
); err != nil {
return nil, fmt.Errorf("failed to register ICA account: %w", err)
}
// Create DEX account record
account := types.InterchainDEXAccount{
Did: did,
ConnectionId: connectionID,
PortId: portID,
EnabledFeatures: features,
Status: types.ACCOUNT_STATUS_PENDING,
CreatedAt: ctx.BlockTime(),
}
// Store account
if err := k.Accounts.Set(ctx, accountKey, account); err != nil {
return nil, fmt.Errorf("failed to store DEX account: %w", err)
}
// Update DID mappings
if err := k.addDIDMapping(ctx, did, connectionID); err != nil {
return nil, fmt.Errorf("failed to update DID mappings: %w", err)
}
return &account, nil
}
// GetDEXAccount retrieves a DEX account by DID and connection
func (k Keeper) GetDEXAccount(
ctx sdk.Context,
did, connectionID string,
) (*types.InterchainDEXAccount, error) {
accountKey := GetAccountKey(did, connectionID)
account, err := k.Accounts.Get(ctx, accountKey)
if err != nil {
return nil, fmt.Errorf("DEX account not found: %w", err)
}
return &account, nil
}
// GetDEXAccountsByDID retrieves all DEX accounts for a DID
func (k Keeper) GetDEXAccountsByDID(
ctx sdk.Context,
did string,
) ([]types.InterchainDEXAccount, error) {
didAccounts, err := k.DIDToAccounts.Get(ctx, did)
if err != nil {
return nil, nil // No accounts for this DID
}
var accounts []types.InterchainDEXAccount
for _, connID := range didAccounts.Accounts {
account, err := k.GetDEXAccount(ctx, did, connID)
if err == nil {
accounts = append(accounts, *account)
}
}
return accounts, nil
}
// SendDEXTransaction sends a transaction through ICA
func (k Keeper) SendDEXTransaction(
ctx sdk.Context,
did string,
connectionID string,
msgs []sdk.Msg,
memo string,
timeoutDuration time.Duration,
) (uint64, error) {
// Get DEX account
account, err := k.GetDEXAccount(ctx, did, connectionID)
if err != nil {
return 0, fmt.Errorf("failed to get DEX account: %w", err)
}
if account.Status != types.ACCOUNT_STATUS_ACTIVE {
return 0, fmt.Errorf("DEX account is not active")
}
// Get ICA address
icaAddress, found := k.icaControllerKeeper.GetInterchainAccountAddress(
ctx,
connectionID,
account.PortId,
)
if !found {
return 0, fmt.Errorf("ICA address not found")
}
// Get channel capability
channelID, found := k.icaControllerKeeper.GetActiveChannelID(ctx, connectionID, account.PortId)
if !found {
return 0, fmt.Errorf("active channel not found")
}
chanCap, ok := k.ScopedKeeper.GetCapability(
ctx,
host.ChannelCapabilityPath(account.PortId, channelID),
)
if !ok {
return 0, fmt.Errorf("channel capability not found")
}
// Encode messages
data, err := icatypes.SerializeCosmosTx(k.cdc, msgs, icatypes.EncodingProtobuf)
if err != nil {
return 0, fmt.Errorf("failed to serialize transaction: %w", err)
}
// Create packet data
packetData := icatypes.InterchainAccountPacketData{
Type: icatypes.EXECUTE_TX,
Data: data,
Memo: memo,
}
// Calculate timeout
timeoutTimestamp := ctx.BlockTime().Add(timeoutDuration).UnixNano()
// Send transaction
sequence, err := k.icaControllerKeeper.SendTx(
ctx,
chanCap,
connectionID,
account.PortId,
packetData,
uint64(timeoutTimestamp),
)
if err != nil {
return 0, fmt.Errorf("failed to send ICA transaction: %w", err)
}
// Log transaction
k.Logger(ctx).Info("DEX transaction sent",
"did", did,
"connection", connectionID,
"ica_address", icaAddress,
"sequence", sequence,
)
return sequence, nil
}
// OnICAAccountCreated handles successful ICA account creation
func (k Keeper) OnICAAccountCreated(ctx sdk.Context, portID, address string) error {
// Find account by port ID
var account *types.InterchainDEXAccount
k.Accounts.Walk(ctx, nil, func(key string, value types.InterchainDEXAccount) (bool, error) {
if value.PortId == portID {
account = &value
return true, nil
}
return false, nil
})
if account == nil {
return fmt.Errorf("DEX account not found for port %s", portID)
}
// Update account status and address
account.Status = types.ACCOUNT_STATUS_ACTIVE
account.AccountAddress = address
account.HostChainId = k.getHostChainID(ctx, account.ConnectionId)
// Store updated account
accountKey := GetAccountKey(account.Did, account.ConnectionId)
if err := k.Accounts.Set(ctx, accountKey, *account); err != nil {
return fmt.Errorf("failed to update DEX account: %w", err)
}
return nil
}
// Helper functions
func (k Keeper) addDIDMapping(ctx sdk.Context, did, connectionID string) error {
didAccounts, _ := k.DIDToAccounts.Get(ctx, did)
// Check if already exists
for _, conn := range didAccounts.Accounts {
if conn == connectionID {
return nil
}
}
didAccounts.Accounts = append(didAccounts.Accounts, connectionID)
return k.DIDToAccounts.Set(ctx, did, didAccounts)
}
func (k Keeper) getHostChainID(ctx sdk.Context, connectionID string) string {
conn, found := k.connectionKeeper.GetConnection(ctx, connectionID)
if !found {
return ""
}
// Extract chain ID from connection counterparty
// This is a simplified version - actual implementation may vary
return conn.Counterparty.ClientId
}
+288
View File
@@ -0,0 +1,288 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/x/dex/types"
)
const (
testConnectionID = "connection-0"
)
// ICAControllerTestSuite tests ICA controller operations
type ICAControllerTestSuite struct {
suite.Suite
f *testFixture
}
func TestICAControllerSuite(t *testing.T) {
suite.Run(t, new(ICAControllerTestSuite))
}
func (suite *ICAControllerTestSuite) SetupTest() {
suite.f = SetupTest(suite.T())
}
// TestRegisterDEXAccount tests ICA account registration
func (suite *ICAControllerTestSuite) TestRegisterDEXAccount() {
did := "did:sonr:test_ica_1"
connectionID := testConnectionID
features := []string{"swap", "liquidity"}
// Register DEX account
account, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
features,
)
suite.Require().NoError(err)
suite.Require().NotNil(account)
// Verify account was created with correct fields
suite.Require().Equal(did, account.Did)
suite.Require().Equal(connectionID, account.ConnectionId)
suite.Require().Equal(types.ACCOUNT_STATUS_PENDING, account.Status)
suite.Require().NotEmpty(account.PortId)
// Verify port ID format
expectedPortPrefix := "dex-" + did
suite.Require().Contains(account.PortId, expectedPortPrefix)
}
// TestRegisterDEXAccount_DuplicateRegistration tests duplicate registration
func (suite *ICAControllerTestSuite) TestRegisterDEXAccount_DuplicateRegistration() {
did := "did:sonr:test_ica_2"
connectionID := testConnectionID
// First registration should succeed
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"swap"},
)
suite.Require().NoError(err)
// Second registration with same DID and connection should return existing account
// (idempotent behavior)
account2, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"swap"},
)
suite.Require().NoError(err)
suite.Require().NotNil(account2)
suite.Require().Equal(did, account2.Did)
suite.Require().Equal(connectionID, account2.ConnectionId)
}
// TestGetDEXAccount tests retrieving a DEX account
func (suite *ICAControllerTestSuite) TestGetDEXAccount() {
did := "did:sonr:test_ica_3"
connectionID := testConnectionID
// Register account first
original, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"order"},
)
suite.Require().NoError(err)
// Retrieve the account
retrieved, err := suite.f.k.GetDEXAccount(suite.f.ctx, did, connectionID)
suite.Require().NoError(err)
suite.Require().NotNil(retrieved)
// Verify retrieved account matches original
suite.Require().Equal(original.Did, retrieved.Did)
suite.Require().Equal(original.ConnectionId, retrieved.ConnectionId)
suite.Require().Equal(original.PortId, retrieved.PortId)
}
// TestGetDEXAccountsByDID tests retrieving all accounts for a DID
func (suite *ICAControllerTestSuite) TestGetDEXAccountsByDID() {
did := "did:sonr:test_ica_4"
connections := []string{testConnectionID, "connection-1", "connection-2"}
// Register multiple accounts for the same DID
for _, connID := range connections {
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connID,
[]string{"swap"},
)
suite.Require().NoError(err)
}
// Retrieve all accounts for the DID
accounts, err := suite.f.k.GetDEXAccountsByDID(suite.f.ctx, did)
suite.Require().NoError(err)
suite.Require().Len(accounts, 3)
// Verify each account has the correct DID
for _, account := range accounts {
suite.Require().Equal(did, account.Did)
}
}
// TestOnICAAccountCreated tests ICA account creation callback
func (suite *ICAControllerTestSuite) TestOnICAAccountCreated() {
did := "did:sonr:test_ica_5"
connectionID := testConnectionID
// Register account first
account, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"swap"},
)
suite.Require().NoError(err)
// Simulate ICA account creation callback
icaAddress := "cosmos1testaddress"
err = suite.f.k.OnICAAccountCreated(
suite.f.ctx,
account.PortId,
icaAddress,
)
suite.Require().NoError(err)
// Verify account was updated
updated, err := suite.f.k.GetDEXAccount(suite.f.ctx, did, connectionID)
suite.Require().NoError(err)
suite.Require().Equal(icaAddress, updated.AccountAddress)
suite.Require().Equal(types.ACCOUNT_STATUS_ACTIVE, updated.Status)
}
// TestSendDEXTransaction tests sending transactions through ICA
func (suite *ICAControllerTestSuite) TestSendDEXTransaction() {
did := "did:sonr:test_ica_6"
connectionID := testConnectionID
// Register account first
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"swap"},
)
suite.Require().NoError(err)
// SendDEXTransaction requires ACTIVE status
// But without full capability module setup, it will fail
// This test just verifies the account must be active
msgs := []sdk.Msg{}
_, err = suite.f.k.SendDEXTransaction(
suite.f.ctx,
did,
connectionID,
msgs,
"test_memo",
30,
)
// Should fail because account is not active
suite.Require().Error(err)
suite.Require().Contains(err.Error(), "not active")
}
// TestPortBinding tests ICA port binding
func (suite *ICAControllerTestSuite) TestPortBinding() {
did := "did:sonr:test_ica_7"
connectionID := testConnectionID
// Register account to trigger port binding
account, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"liquidity"},
)
suite.Require().NoError(err)
// Verify port was bound (mock implementation should handle this)
suite.Require().NotEmpty(account.PortId)
}
// TestConnectionValidation tests connection ID validation
func (suite *ICAControllerTestSuite) TestConnectionValidation() {
did := "did:sonr:test_ica_8"
invalidConnectionID := "invalid-connection"
// Should fail with invalid connection format
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
invalidConnectionID,
[]string{"swap"},
)
// The mock might not validate this, but real implementation would
// This test documents expected behavior
_ = err // Error handling would depend on actual implementation
}
// TestICATimeout tests ICA operation timeout handling
func (suite *ICAControllerTestSuite) TestICATimeout() {
did := "did:sonr:test_ica_9"
connectionID := testConnectionID
// Register account
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"order"},
)
suite.Require().NoError(err)
// Without active account, SendDEXTransaction should fail
msgs := []sdk.Msg{}
_, err = suite.f.k.SendDEXTransaction(
suite.f.ctx,
did,
connectionID,
msgs,
"timeout_test",
1, // 1 second timeout - very short
)
// Should fail because account is not active
suite.Require().Error(err)
suite.Require().Contains(err.Error(), "not active")
}
// TestMultiChainSupport tests support for multiple chains
func (suite *ICAControllerTestSuite) TestMultiChainSupport() {
did := "did:sonr:test_ica_10"
chains := map[string]string{
testConnectionID: "osmosis-1",
"connection-1": "cosmoshub-4",
"connection-2": "juno-1",
}
// Register accounts on multiple chains
for connID := range chains {
account, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connID,
[]string{"swap", "liquidity"},
)
suite.Require().NoError(err)
suite.Require().NotNil(account)
}
// Verify all accounts were created
accounts, err := suite.f.k.GetDEXAccountsByDID(suite.f.ctx, did)
suite.Require().NoError(err)
suite.Require().Len(accounts, 3)
}
+181
View File
@@ -0,0 +1,181 @@
package keeper
import (
"fmt"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/dex/types"
"cosmossdk.io/collections"
"cosmossdk.io/core/store"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitykeeper "github.com/cosmos/ibc-go/modules/capability/keeper"
portkeeper "github.com/cosmos/ibc-go/v8/modules/core/05-port/keeper"
porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types"
ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported"
)
// Keeper defines the DEX module keeper
type Keeper struct {
storeService store.KVStoreService
cdc codec.Codec
schema collections.Schema
authority string
// IBC dependencies
ics4Wrapper porttypes.ICS4Wrapper
PortKeeper *portkeeper.Keeper
ScopedKeeper capabilitykeeper.ScopedKeeper
// External module dependencies
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
icaControllerKeeper types.ICAControllerKeeper
connectionKeeper types.ConnectionKeeper
channelKeeper types.ChannelKeeper
didKeeper types.DIDKeeper
dwnKeeper types.DWNKeeper
// UCAN functionality
ucanVerifier *ucan.Verifier
permissionValidator *PermissionValidator
// Collections for state management
Params collections.Item[types.Params]
Accounts collections.Map[string, types.InterchainDEXAccount]
AccountSequence collections.Sequence
DIDToAccounts collections.Map[string, types.DIDAccounts] // DID -> account mappings
DIDActivities collections.Map[string, types.DEXActivity] // DID activity records
}
// SetDIDKeeper sets the DID keeper (called after initialization)
func (k *Keeper) SetDIDKeeper(didKeeper types.DIDKeeper) {
k.didKeeper = didKeeper
}
// SetDWNKeeper sets the DWN keeper (called after initialization)
func (k *Keeper) SetDWNKeeper(dwnKeeper types.DWNKeeper) {
k.dwnKeeper = dwnKeeper
}
// NewKeeper creates a new DEX Keeper instance
func NewKeeper(
appCodec codec.Codec,
storeService store.KVStoreService,
ics4Wrapper porttypes.ICS4Wrapper,
portKeeper *portkeeper.Keeper,
scopedKeeper capabilitykeeper.ScopedKeeper,
accountKeeper types.AccountKeeper,
bankKeeper types.BankKeeper,
icaControllerKeeper types.ICAControllerKeeper,
connectionKeeper types.ConnectionKeeper,
channelKeeper types.ChannelKeeper,
didKeeper types.DIDKeeper,
dwnKeeper types.DWNKeeper,
authority string,
) Keeper {
sb := collections.NewSchemaBuilder(storeService)
k := Keeper{
cdc: appCodec,
storeService: storeService,
authority: authority,
// IBC dependencies
ics4Wrapper: ics4Wrapper,
PortKeeper: portKeeper,
ScopedKeeper: scopedKeeper,
// External dependencies
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
icaControllerKeeper: icaControllerKeeper,
connectionKeeper: connectionKeeper,
channelKeeper: channelKeeper,
didKeeper: didKeeper,
dwnKeeper: dwnKeeper,
// State collections
Params: collections.NewItem(
sb,
collections.NewPrefix(0),
"params",
codec.CollValue[types.Params](appCodec),
),
Accounts: collections.NewMap(
sb,
collections.NewPrefix(1),
"accounts",
collections.StringKey,
codec.CollValue[types.InterchainDEXAccount](appCodec),
),
AccountSequence: collections.NewSequence(
sb,
collections.NewPrefix(2),
"account_sequence",
),
DIDToAccounts: collections.NewMap(
sb,
collections.NewPrefix(3),
"did_accounts",
collections.StringKey,
codec.CollValue[types.DIDAccounts](appCodec),
),
DIDActivities: collections.NewMap(
sb,
collections.NewPrefix(4),
"did_activities",
collections.StringKey,
codec.CollValue[types.DEXActivity](appCodec),
),
}
schema, err := sb.Build()
if err != nil {
panic(err)
}
k.schema = schema
// Initialize UCAN verifier and permission validator
if didKeeper != nil {
didResolver := &DEXDIDResolver{keeper: k}
k.ucanVerifier = ucan.NewVerifier(didResolver)
k.permissionValidator = NewPermissionValidator(k)
}
return k
}
// WithICS4Wrapper sets the ICS4Wrapper
func (k *Keeper) WithICS4Wrapper(wrapper porttypes.ICS4Wrapper) {
k.ics4Wrapper = wrapper
}
// Logger returns a module-specific logger
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", "x/"+ibcexported.ModuleName+"-"+types.ModuleName)
}
// GetAuthority returns the module authority
func (k Keeper) GetAuthority() string {
return k.authority
}
// GetPermissionValidator returns the UCAN permission validator
func (k Keeper) GetPermissionValidator() *PermissionValidator {
return k.permissionValidator
}
// GetAccountKey generates a unique key for DEX accounts
func GetAccountKey(did, connectionID string) string {
return fmt.Sprintf("%s:%s", did, connectionID)
}
// GetPortID generates a unique port ID for a DEX account
func GetPortID(did, connectionID string) string {
return fmt.Sprintf("dex-%s-%s", did, connectionID)
}
+472
View File
@@ -0,0 +1,472 @@
package keeper_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/suite"
"cosmossdk.io/log"
"cosmossdk.io/math"
storetypes "cosmossdk.io/store/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdkaddress "github.com/cosmos/cosmos-sdk/codec/address"
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/testutil/integration"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
capabilitykeeper "github.com/cosmos/ibc-go/modules/capability/keeper"
capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types"
icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types"
clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
connectiontypes "github.com/cosmos/ibc-go/v8/modules/core/03-connection/types"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
portkeeper "github.com/cosmos/ibc-go/v8/modules/core/05-port/keeper"
ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported"
"github.com/sonr-io/sonr/app"
"github.com/sonr-io/sonr/x/dex/keeper"
"github.com/sonr-io/sonr/x/dex/types"
didtypes "github.com/sonr-io/sonr/x/did/types"
)
var maccPerms = map[string][]string{
authtypes.FeeCollectorName: nil,
stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking},
stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking},
minttypes.ModuleName: {authtypes.Minter},
govtypes.ModuleName: {authtypes.Burner},
}
type testFixture struct {
suite.Suite
ctx sdk.Context
k keeper.Keeper
msgServer types.MsgServer
queryServer types.QueryServer
accountkeeper authkeeper.AccountKeeper
bankkeeper bankkeeper.BaseKeeper
stakingKeeper *stakingkeeper.Keeper
mintkeeper mintkeeper.Keeper
addrs []sdk.AccAddress
govModAddr string
}
// SetupTest creates a new test fixture
func SetupTest(t *testing.T) *testFixture {
t.Helper()
f := new(testFixture)
cfg := sdk.GetConfig()
cfg.SetBech32PrefixForAccount(app.Bech32PrefixAccAddr, app.Bech32PrefixAccPub)
cfg.SetBech32PrefixForValidator(app.Bech32PrefixValAddr, app.Bech32PrefixValPub)
cfg.SetBech32PrefixForConsensusNode(app.Bech32PrefixConsAddr, app.Bech32PrefixConsPub)
cfg.SetCoinType(app.CoinType)
validatorAddressCodec := sdkaddress.NewBech32Codec(app.Bech32PrefixValAddr)
consensusAddressCodec := sdkaddress.NewBech32Codec(app.Bech32PrefixConsAddr)
// Base setup
logger := log.NewTestLogger(t)
encCfg := moduletestutil.MakeTestEncodingConfig()
// Register auth types interfaces
authtypes.RegisterInterfaces(encCfg.InterfaceRegistry)
banktypes.RegisterInterfaces(encCfg.InterfaceRegistry)
stakingtypes.RegisterInterfaces(encCfg.InterfaceRegistry)
minttypes.RegisterInterfaces(encCfg.InterfaceRegistry)
f.govModAddr = authtypes.NewModuleAddress(govtypes.ModuleName).String()
// Initialize test addresses
f.addrs = simtestutil.CreateIncrementalAccounts(3)
// Setup store keys
keys := storetypes.NewKVStoreKeys(
types.StoreKey, authtypes.StoreKey, banktypes.StoreKey,
stakingtypes.StoreKey, minttypes.StoreKey, capabilitytypes.StoreKey,
)
memKeys := storetypes.NewMemoryStoreKeys(capabilitytypes.MemStoreKey)
cdc := encCfg.Codec
// Initialize keepers
authority := authtypes.NewModuleAddress(govtypes.ModuleName)
maccPerms[types.ModuleName] = nil
f.accountkeeper = authkeeper.NewAccountKeeper(
cdc, runtime.NewKVStoreService(keys[authtypes.StoreKey]),
authtypes.ProtoBaseAccount, maccPerms,
sdkaddress.NewBech32Codec(app.Bech32PrefixAccAddr),
app.Bech32PrefixAccAddr, authority.String(),
)
f.bankkeeper = bankkeeper.NewBaseKeeper(
cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]),
f.accountkeeper, nil, authority.String(), logger,
)
f.stakingKeeper = stakingkeeper.NewKeeper(
cdc, runtime.NewKVStoreService(keys[stakingtypes.StoreKey]),
f.accountkeeper, f.bankkeeper, authority.String(),
validatorAddressCodec, consensusAddressCodec,
)
f.mintkeeper = mintkeeper.NewKeeper(
cdc, runtime.NewKVStoreService(keys[minttypes.StoreKey]),
f.stakingKeeper, f.accountkeeper, f.bankkeeper,
authtypes.FeeCollectorName, authority.String(),
)
// Create capability keeper for IBC
capabilityKeeper := capabilitykeeper.NewKeeper(
cdc,
keys[capabilitytypes.StoreKey],
memKeys[capabilitytypes.MemStoreKey],
)
// Create scoped keeper for the DEX module
scopedKeeper := capabilityKeeper.ScopeToModule(types.ModuleName)
// Create port keeper
portKeeper := portkeeper.NewKeeper(scopedKeeper)
// Create mock expected keepers
mockICS4Wrapper := &mockICS4Wrapper{}
mockAccountKeeper := &mockAccountKeeper{}
mockBankKeeper := &mockBankKeeper{}
mockICAControllerKeeper := &mockICAControllerKeeper{}
mockConnectionKeeper := &mockConnectionKeeper{}
mockChannelKeeper := &mockChannelKeeper{}
mockDIDKeeper := &mockDIDKeeper{}
mockDWNKeeper := &mockDWNKeeper{}
// Initialize DEX keeper
f.k = keeper.NewKeeper(
cdc,
runtime.NewKVStoreService(keys[types.StoreKey]),
mockICS4Wrapper,
&portKeeper,
scopedKeeper,
mockAccountKeeper,
mockBankKeeper,
mockICAControllerKeeper,
mockConnectionKeeper,
mockChannelKeeper,
mockDIDKeeper,
mockDWNKeeper,
authority.String(),
)
f.msgServer = keeper.NewMsgServerImpl(f.k)
f.queryServer = keeper.NewQueryServerImpl(f.k)
// Initialize context with proper multistore
cms := integration.CreateMultiStore(keys, logger)
for _, key := range memKeys {
cms.MountStoreWithDB(key, storetypes.StoreTypeMemory, nil)
}
f.ctx = sdk.NewContext(cms, cmtproto.Header{
Height: 1,
Time: time.Now(),
}, false, logger)
// Fund test accounts
initCoins := sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000000000)))
for _, addr := range f.addrs {
err := f.bankkeeper.MintCoins(f.ctx, minttypes.ModuleName, initCoins)
if err != nil {
panic(err)
}
err = f.bankkeeper.SendCoinsFromModuleToAccount(
f.ctx,
minttypes.ModuleName,
addr,
initCoins,
)
if err != nil {
panic(err)
}
}
return f
}
// KeeperTestSuite runs all keeper tests
type KeeperTestSuite struct {
suite.Suite
f *testFixture
}
func TestKeeperSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}
func (suite *KeeperTestSuite) SetupTest() {
suite.f = SetupTest(suite.T())
}
// Test basic keeper operations
func (suite *KeeperTestSuite) TestRegisterDEXAccount() {
did := "did:sonr:test123"
connectionID := "connection-0"
// Register a new DEX account through keeper method
account, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connectionID,
[]string{"swap", "liquidity"},
)
suite.Require().NoError(err)
suite.Require().NotNil(account)
// Retrieve the account
retrieved, err := suite.f.k.GetDEXAccount(suite.f.ctx, did, connectionID)
suite.Require().NoError(err)
suite.Require().NotNil(retrieved)
suite.Require().Equal(did, retrieved.Did)
suite.Require().Equal(connectionID, retrieved.ConnectionId)
suite.Require().Equal(types.ACCOUNT_STATUS_PENDING, retrieved.Status)
}
func (suite *KeeperTestSuite) TestGetDEXAccountsByDID() {
did := "did:sonr:test456"
// Register multiple accounts for the same DID
connections := []string{"connection-0", "connection-1"}
for _, connID := range connections {
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
did,
connID,
[]string{"swap"},
)
suite.Require().NoError(err)
}
// Retrieve all accounts for the DID
accounts, err := suite.f.k.GetDEXAccountsByDID(suite.f.ctx, did)
suite.Require().NoError(err)
suite.Require().Len(accounts, 2)
}
func (suite *KeeperTestSuite) TestParamsOperations() {
// Set params
params := types.Params{
Enabled: true,
MaxAccountsPerDid: 5,
DefaultTimeoutSeconds: 600,
AllowedConnections: []string{"connection-0", "connection-1"},
MinSwapAmount: "100",
MaxDailyVolume: "1000000",
RateLimits: types.RateLimitParams{
MaxOpsPerBlock: 10,
MaxOpsPerDidPerDay: 100,
CooldownBlocks: 5,
},
Fees: types.FeeParams{
SwapFeeBps: 30, // 0.3%
LiquidityFeeBps: 10, // 0.1%
OrderFeeBps: 20, // 0.2%
FeeCollector: "sonr1feecolllector",
},
}
err := suite.f.k.Params.Set(suite.f.ctx, params)
suite.Require().NoError(err)
// Get params
retrieved, err := suite.f.k.Params.Get(suite.f.ctx)
suite.Require().NoError(err)
suite.Require().Equal(params.Enabled, retrieved.Enabled)
suite.Require().Equal(params.MaxAccountsPerDid, retrieved.MaxAccountsPerDid)
suite.Require().Equal(params.AllowedConnections, retrieved.AllowedConnections)
}
// Mock implementations for expected keepers
type mockICS4Wrapper struct{}
func (m *mockICS4Wrapper) SendPacket(
ctx sdk.Context,
channelCap *capabilitytypes.Capability,
sourcePort string,
sourceChannel string,
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
data []byte,
) (uint64, error) {
return 1, nil
}
func (m *mockICS4Wrapper) WriteAcknowledgement(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
packet ibcexported.PacketI,
acknowledgement ibcexported.Acknowledgement,
) error {
return nil
}
func (m *mockICS4Wrapper) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) {
return "ics27-1", true
}
type mockAccountKeeper struct{}
func (m *mockAccountKeeper) GetAccount(ctx context.Context, addr sdk.AccAddress) sdk.AccountI {
return nil
}
func (m *mockAccountKeeper) SetAccount(ctx context.Context, acc sdk.AccountI) {}
func (m *mockAccountKeeper) NewAccountWithAddress(
ctx sdk.Context,
addr sdk.AccAddress,
) sdk.AccountI {
return nil
}
func (m *mockAccountKeeper) GetModuleAccount(
ctx context.Context,
moduleName string,
) sdk.ModuleAccountI {
return nil
}
func (m *mockAccountKeeper) GetModuleAddress(name string) sdk.AccAddress {
return sdk.AccAddress{}
}
type mockBankKeeper struct{}
func (m *mockBankKeeper) SendCoins(
ctx context.Context,
fromAddr, toAddr sdk.AccAddress,
amt sdk.Coins,
) error {
return nil
}
func (m *mockBankKeeper) SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins {
return sdk.NewCoins()
}
type mockICAControllerKeeper struct{}
func (m *mockICAControllerKeeper) RegisterInterchainAccount(
ctx sdk.Context,
connectionID, owner, version string,
) error {
return nil
}
func (m *mockICAControllerKeeper) GetInterchainAccountAddress(
ctx sdk.Context,
connectionID, portID string,
) (string, bool) {
return "cosmos1test", true
}
func (m *mockICAControllerKeeper) SendTx(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
connectionID, portID string,
icaPacketData icatypes.InterchainAccountPacketData,
timeoutTimestamp uint64,
) (uint64, error) {
return 1, nil
}
func (m *mockICAControllerKeeper) GetActiveChannelID(
ctx sdk.Context,
connectionID, portID string,
) (string, bool) {
return "channel-0", true
}
type mockConnectionKeeper struct{}
func (m *mockConnectionKeeper) GetConnection(
ctx sdk.Context,
connectionID string,
) (connectiontypes.ConnectionEnd, bool) {
return connectiontypes.ConnectionEnd{
ClientId: "07-tendermint-0",
Versions: []*connectiontypes.Version{{
Identifier: "1",
Features: []string{"ORDER_ORDERED", "ORDER_UNORDERED"},
}},
State: connectiontypes.OPEN,
Counterparty: connectiontypes.Counterparty{
ClientId: "07-tendermint-0",
ConnectionId: "connection-0",
},
}, true
}
type mockChannelKeeper struct{}
func (m *mockChannelKeeper) GetChannel(
ctx sdk.Context,
portID, channelID string,
) (channeltypes.Channel, bool) {
return channeltypes.Channel{
State: channeltypes.OPEN,
Ordering: channeltypes.ORDERED,
Counterparty: channeltypes.Counterparty{
PortId: "icahost",
ChannelId: "channel-0",
},
ConnectionHops: []string{"connection-0"},
Version: "ics27-1",
}, true
}
func (m *mockChannelKeeper) GetNextSequenceSend(
ctx sdk.Context,
portID, channelID string,
) (uint64, bool) {
return 1, true
}
func (m *mockChannelKeeper) SendPacket(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
sourcePort string,
sourceChannel string,
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
data []byte,
) (uint64, error) {
return 1, nil
}
type mockDIDKeeper struct{}
func (m *mockDIDKeeper) GetDIDDocument(
ctx context.Context,
did string,
) (*didtypes.DIDDocument, error) {
return &didtypes.DIDDocument{
Id: did,
}, nil
}
type mockDWNKeeper struct{}
+191
View File
@@ -0,0 +1,191 @@
package keeper
import (
"fmt"
"time"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/sonr-io/sonr/x/dex/types"
)
// ProvideLiquidity handles liquidity provision through ICA
func (k Keeper) ProvideLiquidity(
ctx sdk.Context,
did string,
connectionID string,
poolID uint64,
tokenA sdk.Coin,
tokenB sdk.Coin,
minShares math.Int,
) (uint64, error) {
// Get the DEX account
account, err := k.GetDEXAccount(ctx, did, connectionID)
if err != nil {
return 0, fmt.Errorf("DEX account not found: %w", err)
}
// Verify account is active
if account.Status != types.ACCOUNT_STATUS_ACTIVE {
return 0, fmt.Errorf("DEX account is not active")
}
// Create liquidity provision message for remote chain
// This is a placeholder - actual implementation would use chain-specific messages
lpMsg := &banktypes.MsgSend{
FromAddress: account.AccountAddress,
ToAddress: account.AccountAddress, // Placeholder
Amount: sdk.NewCoins(tokenA, tokenB),
}
// Send the liquidity transaction via ICA
sequence, err := k.SendDEXTransaction(
ctx,
did,
connectionID,
[]sdk.Msg{lpMsg},
fmt.Sprintf("provide_liquidity_pool_%d", poolID),
30*time.Second,
)
if err != nil {
return 0, fmt.Errorf("failed to send liquidity transaction: %w", err)
}
// Emit liquidity event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeLiquidityProvided,
sdk.NewAttribute("did", did),
sdk.NewAttribute("connection", connectionID),
sdk.NewAttribute("pool_id", fmt.Sprintf("%d", poolID)),
sdk.NewAttribute("token_a", tokenA.String()),
sdk.NewAttribute("token_b", tokenB.String()),
sdk.NewAttribute("sequence", fmt.Sprintf("%d", sequence)),
),
)
return sequence, nil
}
// RemoveLiquidity handles liquidity removal through ICA
func (k Keeper) RemoveLiquidity(
ctx sdk.Context,
did string,
connectionID string,
poolID uint64,
shares math.Int,
minAmountA math.Int,
minAmountB math.Int,
) (uint64, error) {
// Get the DEX account
account, err := k.GetDEXAccount(ctx, did, connectionID)
if err != nil {
return 0, fmt.Errorf("DEX account not found: %w", err)
}
// Verify account is active
if account.Status != types.ACCOUNT_STATUS_ACTIVE {
return 0, fmt.Errorf("DEX account is not active")
}
// Create liquidity removal message for remote chain
// This is a placeholder - actual implementation would use chain-specific messages
removeMsg := &banktypes.MsgSend{
FromAddress: account.AccountAddress,
ToAddress: account.AccountAddress, // Placeholder
Amount: sdk.NewCoins(sdk.NewCoin("shares", shares)),
}
// Send the removal transaction via ICA
sequence, err := k.SendDEXTransaction(
ctx,
did,
connectionID,
[]sdk.Msg{removeMsg},
fmt.Sprintf("remove_liquidity_pool_%d", poolID),
30*time.Second,
)
if err != nil {
return 0, fmt.Errorf("failed to send liquidity removal transaction: %w", err)
}
// Emit removal event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeLiquidityRemoved,
sdk.NewAttribute("did", did),
sdk.NewAttribute("connection", connectionID),
sdk.NewAttribute("pool_id", fmt.Sprintf("%d", poolID)),
sdk.NewAttribute("shares", shares.String()),
sdk.NewAttribute("sequence", fmt.Sprintf("%d", sequence)),
),
)
return sequence, nil
}
// EstimateLPShares estimates the LP shares for given liquidity
func (k Keeper) EstimateLPShares(
ctx sdk.Context,
connectionID string,
poolID uint64,
tokenA sdk.Coin,
tokenB sdk.Coin,
) (math.Int, error) {
// This would query the remote chain for LP share estimation
// For now, return a placeholder value
totalValue := tokenA.Amount.Add(tokenB.Amount)
return totalValue.QuoRaw(2), nil // Simple average as placeholder
}
// GetPoolInfo retrieves pool information from remote chain
func (k Keeper) GetPoolInfo(
ctx sdk.Context,
connectionID string,
poolID uint64,
) (*PoolInfo, error) {
// This would query the remote chain for pool info
// For now, return placeholder data
return &PoolInfo{
PoolID: poolID,
TokenA: "uatom",
TokenB: "uosmo",
TotalShares: math.NewInt(1000000),
TotalLiquidity: sdk.NewCoins(
sdk.NewCoin("uatom", math.NewInt(500000)),
sdk.NewCoin("uosmo", math.NewInt(500000)),
),
}, nil
}
// PoolInfo represents pool information
type PoolInfo struct {
PoolID uint64
TokenA string
TokenB string
TotalShares math.Int
TotalLiquidity sdk.Coins
}
// ValidateLiquidityParameters validates liquidity parameters
func (k Keeper) ValidateLiquidityParameters(
tokenA sdk.Coin,
tokenB sdk.Coin,
minShares math.Int,
) error {
if tokenA.IsZero() || tokenB.IsZero() {
return fmt.Errorf("token amounts cannot be zero")
}
if tokenA.Denom == tokenB.Denom {
return fmt.Errorf("cannot provide liquidity with same token")
}
if minShares.IsNegative() {
return fmt.Errorf("minimum shares cannot be negative")
}
return nil
}
+219
View File
@@ -0,0 +1,219 @@
package keeper
import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/x/dex/types"
)
var _ types.MsgServer = msgServer{}
type msgServer struct {
Keeper
}
// NewMsgServerImpl returns an implementation of the module MsgServer interface.
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
return &msgServer{Keeper: keeper}
}
// RegisterDEXAccount implements types.MsgServer.
func (ms msgServer) RegisterDEXAccount(
ctx context.Context,
msg *types.MsgRegisterDEXAccount,
) (*types.MsgRegisterDEXAccountResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
// Register the DEX account using the keeper's ICA controller logic
account, err := ms.Keeper.RegisterDEXAccount(
sdkCtx,
msg.Did,
msg.ConnectionId,
msg.Features,
)
if err != nil {
return nil, err
}
// Emit event for account registration
sdkCtx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeDEXAccountRegistered,
sdk.NewAttribute("did", msg.Did),
sdk.NewAttribute("connection_id", msg.ConnectionId),
sdk.NewAttribute("port_id", account.PortId),
),
)
return &types.MsgRegisterDEXAccountResponse{
PortId: account.PortId,
AccountAddress: account.AccountAddress,
}, nil
}
// TODO: ExecuteSwap - Implement cross-chain swap execution via ICA
// This method should handle token swaps on remote chains through Interchain Accounts
// Required implementation steps:
// 1. Validate the sender's DID exists and is active using did keeper
// 2. Verify UCAN token has proper swap capabilities (resource: swap, action: execute)
// 3. Retrieve the ICA account for this DID and connection from state
// 4. Build the appropriate swap message for the target chain's DEX protocol
// 5. Create ICA packet data with the swap transaction
// 6. Send ICA packet through IBC channel and await acknowledgment
// 7. Store transaction details in DWN for user history tracking
// 8. Emit events for indexing and monitoring
// Returns: Sequence number and transaction ID on success
// ExecuteSwap implements types.MsgServer.
func (ms msgServer) ExecuteSwap(
ctx context.Context,
msg *types.MsgExecuteSwap,
) (*types.MsgExecuteSwapResponse, error) {
// Validate UCAN permission if token provided
if msg.UcanToken != "" {
// Use connection ID as resource ID for swap operations
if err := ms.validateUCANPermission(ctx, msg.UcanToken, "swap", msg.ConnectionId, types.DEXOpExecuteSwap); err != nil {
return nil, err
}
}
// TODO: Implement swap execution via ICA
// 1. Validate DID
// 2. Get ICA account for this DID and connection
// 3. Construct swap message for remote chain
// 4. Send ICA packet with swap instruction
// 5. Track transaction in DWN
return &types.MsgExecuteSwapResponse{}, nil
}
// validateUCANPermission validates UCAN token for a DEX operation
func (ms msgServer) validateUCANPermission(
ctx context.Context,
ucanToken string,
resourceType string,
resourceID string,
operation types.DEXOperation,
) error {
if ms.permissionValidator == nil {
// Permission validator not available - skip validation
return nil
}
return ms.permissionValidator.ValidatePermission(
ctx,
ucanToken,
resourceType,
resourceID,
operation,
)
}
// TODO: ProvideLiquidity - Implement cross-chain liquidity provision via ICA
// This method should handle adding liquidity to pools on remote chains
// Required implementation steps:
// 1. Validate the sender's DID exists and is active using did keeper
// 2. Verify UCAN token has liquidity provision capabilities (resource: liquidity, action: provide)
// 3. Retrieve the ICA account for this DID and connection from state
// 4. Calculate appropriate liquidity amounts based on pool ratios
// 5. Build liquidity provision message for target chain's AMM protocol
// 6. Create ICA packet data with the liquidity transaction
// 7. Send ICA packet through IBC channel and await acknowledgment
// 8. Store LP token information in DWN for tracking
// 9. Update user's position records in state
// Returns: Sequence number and LP token amount on success
// ProvideLiquidity implements types.MsgServer.
func (ms msgServer) ProvideLiquidity(
ctx context.Context,
msg *types.MsgProvideLiquidity,
) (*types.MsgProvideLiquidityResponse, error) {
// TODO: Implement liquidity provision via ICA
// 1. Validate DID and UCAN token
// 2. Get ICA account for this DID and connection
// 3. Construct liquidity provision message for remote chain
// 4. Send ICA packet with liquidity instruction
// 5. Track transaction in DWN
return &types.MsgProvideLiquidityResponse{}, nil
}
// TODO: RemoveLiquidity - Implement cross-chain liquidity removal via ICA
// This method should handle removing liquidity from pools on remote chains
// Required implementation steps:
// 1. Validate the sender's DID exists and is active using did keeper
// 2. Verify UCAN token has liquidity removal capabilities (resource: liquidity, action: remove)
// 3. Retrieve the ICA account for this DID and connection from state
// 4. Verify user has sufficient LP tokens to remove
// 5. Build liquidity removal message for target chain's AMM protocol
// 6. Create ICA packet data with the removal transaction
// 7. Send ICA packet through IBC channel and await acknowledgment
// 8. Update LP token information in DWN after removal
// 9. Clear user's position records from state if fully withdrawn
// Returns: Sequence number and withdrawn token amounts on success
// RemoveLiquidity implements types.MsgServer.
func (ms msgServer) RemoveLiquidity(
ctx context.Context,
msg *types.MsgRemoveLiquidity,
) (*types.MsgRemoveLiquidityResponse, error) {
// TODO: Implement liquidity removal via ICA
// 1. Validate DID and UCAN token
// 2. Get ICA account for this DID and connection
// 3. Construct liquidity removal message for remote chain
// 4. Send ICA packet with removal instruction
// 5. Track transaction in DWN
return &types.MsgRemoveLiquidityResponse{}, nil
}
// TODO: CreateLimitOrder - Implement cross-chain limit order creation via ICA
// This method should handle placing limit orders on remote chain order books
// Required implementation steps:
// 1. Validate the sender's DID exists and is active using did keeper
// 2. Verify UCAN token has order creation capabilities (resource: order, action: create)
// 3. Retrieve the ICA account for this DID and connection from state
// 4. Validate order parameters (price, amount, expiry) against market conditions
// 5. Build limit order message for target chain's order book protocol
// 6. Create ICA packet data with the order placement transaction
// 7. Send ICA packet through IBC channel and await acknowledgment
// 8. Store order details in local state for tracking
// 9. Create order record in DWN with unique order ID
// 10. Set up monitoring for order fills and expiration
// Returns: Sequence number and unique order ID on success
// CreateLimitOrder implements types.MsgServer.
func (ms msgServer) CreateLimitOrder(
ctx context.Context,
msg *types.MsgCreateLimitOrder,
) (*types.MsgCreateLimitOrderResponse, error) {
// TODO: Implement limit order creation via ICA
// 1. Validate DID and UCAN token
// 2. Get ICA account for this DID and connection
// 3. Construct limit order message for remote chain
// 4. Send ICA packet with order instruction
// 5. Track order in DWN
return &types.MsgCreateLimitOrderResponse{}, nil
}
// TODO: CancelOrder - Implement cross-chain order cancellation via ICA
// This method should handle cancelling existing limit orders on remote chains
// Required implementation steps:
// 1. Validate the sender's DID exists and is active using did keeper
// 2. Verify UCAN token has order cancellation capabilities (resource: order, action: cancel)
// 3. Retrieve the ICA account for this DID and connection from state
// 4. Verify the order exists and belongs to the sender
// 5. Check order status is still open (not filled or already cancelled)
// 6. Build order cancellation message for target chain's order book protocol
// 7. Create ICA packet data with the cancellation transaction
// 8. Send ICA packet through IBC channel and await acknowledgment
// 9. Update order status in local state to cancelled
// 10. Update order record in DWN with cancellation details
// Returns: Sequence number on successful cancellation
// CancelOrder implements types.MsgServer.
func (ms msgServer) CancelOrder(
ctx context.Context,
msg *types.MsgCancelOrder,
) (*types.MsgCancelOrderResponse, error) {
// TODO: Implement order cancellation via ICA
// 1. Validate DID and UCAN token
// 2. Get ICA account for this DID and connection
// 3. Construct order cancellation message for remote chain
// 4. Send ICA packet with cancellation instruction
// 5. Update order status in DWN
return &types.MsgCancelOrderResponse{}, nil
}
+316
View File
@@ -0,0 +1,316 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/x/dex/keeper"
"github.com/sonr-io/sonr/x/dex/types"
)
// MsgServerTestSuite tests message server operations
type MsgServerTestSuite struct {
suite.Suite
f *testFixture
}
func TestMsgServerSuite(t *testing.T) {
suite.Run(t, new(MsgServerTestSuite))
}
func (suite *MsgServerTestSuite) SetupTest() {
suite.f = SetupTest(suite.T())
}
// TestMsgRegisterDEXAccount tests the RegisterDEXAccount message handler
func (suite *MsgServerTestSuite) TestMsgRegisterDEXAccount() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// Create test message
msg := &types.MsgRegisterDEXAccount{
Did: "did:sonr:alice",
ConnectionId: "connection-0",
Features: []string{"swap", "liquidity"},
}
// Execute message
resp, err := msgServer.RegisterDEXAccount(ctx, msg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
suite.Require().NotEmpty(resp.PortId)
// Verify account was created
account, err := suite.f.k.GetDEXAccount(suite.f.ctx, msg.Did, msg.ConnectionId)
suite.Require().NoError(err)
suite.Require().NotNil(account)
suite.Require().Equal(msg.Did, account.Did)
suite.Require().Equal(msg.ConnectionId, account.ConnectionId)
}
// TestMsgExecuteSwap tests the ExecuteSwap message handler
func (suite *MsgServerTestSuite) TestMsgExecuteSwap() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// First register an account
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
"did:sonr:bob",
"connection-0",
[]string{"swap"},
)
suite.Require().NoError(err)
// Create swap message
msg := &types.MsgExecuteSwap{
Did: "did:sonr:bob",
ConnectionId: "connection-0",
SourceDenom: "usnr",
TargetDenom: "uosmo",
Amount: math.NewInt(1000),
MinAmountOut: math.NewInt(900),
Route: "pool:1",
}
// Execute swap
resp, err := msgServer.ExecuteSwap(ctx, msg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
// TODO: Check sequence when ExecuteSwap is implemented
// suite.Require().NotZero(resp.Sequence)
}
// TestMsgProvideLiquidity tests the ProvideLiquidity message handler
func (suite *MsgServerTestSuite) TestMsgProvideLiquidity() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// First register an account
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
"did:sonr:charlie",
"connection-0",
[]string{"liquidity"},
)
suite.Require().NoError(err)
// Create liquidity message
msg := &types.MsgProvideLiquidity{
Did: "did:sonr:charlie",
ConnectionId: "connection-0",
PoolId: "1",
Assets: sdk.NewCoins(
sdk.NewCoin("usnr", math.NewInt(1000)),
sdk.NewCoin("uosmo", math.NewInt(1000)),
),
MinShares: math.NewInt(100),
Timeout: time.Now().Add(5 * time.Minute),
}
// Execute liquidity provision
resp, err := msgServer.ProvideLiquidity(ctx, msg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
// TODO: Check sequence when ProvideLiquidity is implemented
// suite.Require().NotZero(resp.Sequence)
}
// TestMsgRemoveLiquidity tests the RemoveLiquidity message handler
func (suite *MsgServerTestSuite) TestMsgRemoveLiquidity() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// First register an account
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
"did:sonr:dave",
"connection-0",
[]string{"liquidity"},
)
suite.Require().NoError(err)
// Create remove liquidity message
msg := &types.MsgRemoveLiquidity{
Did: "did:sonr:dave",
ConnectionId: "connection-0",
PoolId: "1",
Shares: math.NewInt(100),
MinAmounts: sdk.NewCoins(
sdk.NewCoin("usnr", math.NewInt(900)),
sdk.NewCoin("uosmo", math.NewInt(900)),
),
Timeout: time.Now().Add(5 * time.Minute),
}
// Execute liquidity removal
resp, err := msgServer.RemoveLiquidity(ctx, msg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
// TODO: Check sequence when RemoveLiquidity is implemented
// suite.Require().NotZero(resp.Sequence)
}
// TestMsgCreateLimitOrder tests the CreateLimitOrder message handler
func (suite *MsgServerTestSuite) TestMsgCreateLimitOrder() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// First register an account
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
"did:sonr:eve",
"connection-0",
[]string{"order"},
)
suite.Require().NoError(err)
// Create limit order message
msg := &types.MsgCreateLimitOrder{
Did: "did:sonr:eve",
ConnectionId: "connection-0",
SellDenom: "usnr",
BuyDenom: "uosmo",
Amount: math.NewInt(1000),
Price: math.LegacyNewDec(1),
Expiration: time.Now().Add(24 * time.Hour),
}
// Execute order creation
resp, err := msgServer.CreateLimitOrder(ctx, msg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
// TODO: Check sequence and OrderId when CreateLimitOrder is implemented
// suite.Require().NotZero(resp.Sequence)
// suite.Require().NotEmpty(resp.OrderId)
}
// TestMsgCancelOrder tests the CancelOrder message handler
func (suite *MsgServerTestSuite) TestMsgCancelOrder() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// First register an account and create an order
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
"did:sonr:frank",
"connection-0",
[]string{"order"},
)
suite.Require().NoError(err)
// Since CreateLimitOrder is not implemented yet, use a mock order ID
mockOrderId := "order-123"
// Cancel the order
cancelMsg := &types.MsgCancelOrder{
Did: "did:sonr:frank",
ConnectionId: "connection-0",
OrderId: mockOrderId,
}
// Execute order cancellation
resp, err := msgServer.CancelOrder(ctx, cancelMsg)
suite.Require().NoError(err)
suite.Require().NotNil(resp)
// TODO: Check sequence when CancelOrder is implemented
// suite.Require().NotZero(resp.Sequence)
}
// TestMsgRegisterDEXAccount_InvalidDID tests registration with invalid DID
func (suite *MsgServerTestSuite) TestMsgRegisterDEXAccount_InvalidDID() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// Create test message with invalid DID
msg := &types.MsgRegisterDEXAccount{
Did: "", // Empty DID
ConnectionId: "connection-0",
Features: []string{"swap"},
}
// Should fail validation
_, err := msgServer.RegisterDEXAccount(ctx, msg)
suite.Require().Error(err)
}
// TestMsgExecuteSwap_AccountNotFound tests swap with non-existent account
func (suite *MsgServerTestSuite) TestMsgExecuteSwap_AccountNotFound() {
msgServer := keeper.NewMsgServerImpl(suite.f.k)
ctx := sdk.WrapSDKContext(suite.f.ctx)
// Create swap message without registering account
msg := &types.MsgExecuteSwap{
Did: "did:sonr:nonexistent",
ConnectionId: "connection-0",
SourceDenom: "usnr",
TargetDenom: "uosmo",
Amount: math.NewInt(1000),
MinAmountOut: math.NewInt(900),
Route: "pool:1",
}
// TODO: Should fail when ExecuteSwap is implemented - account not found
_, err := msgServer.ExecuteSwap(ctx, msg)
suite.Require().NoError(err) // Currently returns empty response
// suite.Require().Error(err)
// suite.Require().Contains(err.Error(), "not found")
}
// TestMsgProvideLiquidity_InvalidAssets tests liquidity with invalid assets
func (suite *MsgServerTestSuite) TestMsgProvideLiquidity_InvalidAssets() {
// First register an account
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
"did:sonr:grace",
"connection-0",
[]string{"liquidity"},
)
suite.Require().NoError(err)
// Create liquidity message with empty assets
msg := &types.MsgProvideLiquidity{
Did: "did:sonr:grace",
ConnectionId: "connection-0",
PoolId: "1",
Assets: sdk.NewCoins(), // Empty coins list
MinShares: math.NewInt(100),
Timeout: time.Now().Add(5 * time.Minute),
}
// Should fail validation due to empty assets
err = msg.ValidateBasic()
suite.Require().Error(err)
}
// TestMsgCreateLimitOrder_InvalidPrice tests order creation with invalid price
func (suite *MsgServerTestSuite) TestMsgCreateLimitOrder_InvalidPrice() {
// First register an account
_, err := suite.f.k.RegisterDEXAccount(
suite.f.ctx,
"did:sonr:henry",
"connection-0",
[]string{"order"},
)
suite.Require().NoError(err)
// Create limit order message with zero price
msg := &types.MsgCreateLimitOrder{
Did: "did:sonr:henry",
ConnectionId: "connection-0",
SellDenom: "usnr",
BuyDenom: "uosmo",
Amount: math.NewInt(1000),
Price: math.LegacyZeroDec(), // Invalid: zero price
Expiration: time.Now().Add(24 * time.Hour),
}
// Should fail validation
err = msg.ValidateBasic()
suite.Require().Error(err)
}
+231
View File
@@ -0,0 +1,231 @@
package keeper
import (
"fmt"
"time"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/sonr-io/sonr/x/dex/types"
)
// CreateLimitOrder creates a limit order through ICA
func (k Keeper) CreateLimitOrder(
ctx sdk.Context,
did string,
connectionID string,
tokenIn sdk.Coin,
tokenOutDenom string,
price math.LegacyDec,
orderType OrderType,
) (uint64, error) {
// Get the DEX account
account, err := k.GetDEXAccount(ctx, did, connectionID)
if err != nil {
return 0, fmt.Errorf("DEX account not found: %w", err)
}
// Verify account is active
if account.Status != types.ACCOUNT_STATUS_ACTIVE {
return 0, fmt.Errorf("DEX account is not active")
}
// Create limit order message for remote chain
// This is a placeholder - actual implementation would use chain-specific messages
orderMsg := &banktypes.MsgSend{
FromAddress: account.AccountAddress,
ToAddress: account.AccountAddress, // Placeholder
Amount: sdk.NewCoins(tokenIn),
}
// Send the order transaction via ICA
sequence, err := k.SendDEXTransaction(
ctx,
did,
connectionID,
[]sdk.Msg{orderMsg},
fmt.Sprintf("limit_order_%s_for_%s", tokenIn.Denom, tokenOutDenom),
30*time.Second,
)
if err != nil {
return 0, fmt.Errorf("failed to send order transaction: %w", err)
}
// Store order ID mapping (sequence -> order details)
orderID := fmt.Sprintf("%s_%s_%d", did, connectionID, sequence)
// Emit order created event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeOrderCreated,
sdk.NewAttribute("did", did),
sdk.NewAttribute("connection", connectionID),
sdk.NewAttribute("order_id", orderID),
sdk.NewAttribute("token_in", tokenIn.String()),
sdk.NewAttribute("token_out", tokenOutDenom),
sdk.NewAttribute("price", price.String()),
sdk.NewAttribute("sequence", fmt.Sprintf("%d", sequence)),
),
)
return sequence, nil
}
// CancelOrder cancels an existing order through ICA
func (k Keeper) CancelOrder(
ctx sdk.Context,
did string,
connectionID string,
orderID string,
) (uint64, error) {
// Get the DEX account
account, err := k.GetDEXAccount(ctx, did, connectionID)
if err != nil {
return 0, fmt.Errorf("DEX account not found: %w", err)
}
// Verify account is active
if account.Status != types.ACCOUNT_STATUS_ACTIVE {
return 0, fmt.Errorf("DEX account is not active")
}
// Create cancel order message for remote chain
// This is a placeholder - actual implementation would use chain-specific messages
cancelMsg := &banktypes.MsgSend{
FromAddress: account.AccountAddress,
ToAddress: account.AccountAddress, // Placeholder
Amount: sdk.NewCoins(), // Empty amount for cancel
}
// Send the cancel transaction via ICA
sequence, err := k.SendDEXTransaction(
ctx,
did,
connectionID,
[]sdk.Msg{cancelMsg},
fmt.Sprintf("cancel_order_%s", orderID),
30*time.Second,
)
if err != nil {
return 0, fmt.Errorf("failed to send cancel transaction: %w", err)
}
// Emit order cancelled event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeOrderCancelled,
sdk.NewAttribute("did", did),
sdk.NewAttribute("connection", connectionID),
sdk.NewAttribute("order_id", orderID),
sdk.NewAttribute("sequence", fmt.Sprintf("%d", sequence)),
),
)
return sequence, nil
}
// OrderType represents the type of order
type OrderType int
const (
OrderTypeLimit OrderType = iota
OrderTypeMarket
OrderTypeStopLoss
OrderTypeTakeProfit
)
// OrderStatus represents the status of an order
type OrderStatus int
const (
OrderStatusPending OrderStatus = iota
OrderStatusOpen
OrderStatusPartiallyFilled
OrderStatusFilled
OrderStatusCancelled
OrderStatusExpired
)
// OrderInfo represents order information
type OrderInfo struct {
OrderID string
DID string
ConnectionID string
TokenIn sdk.Coin
TokenOut string
Price math.LegacyDec
Type OrderType
Status OrderStatus
FilledAmount math.Int
RemainingAmount math.Int
CreatedAt int64
UpdatedAt int64
}
// GetOrderInfo retrieves order information
func (k Keeper) GetOrderInfo(
ctx sdk.Context,
did string,
connectionID string,
orderID string,
) (*OrderInfo, error) {
// This would retrieve order info from state or remote chain
// For now, return placeholder data
return &OrderInfo{
OrderID: orderID,
DID: did,
ConnectionID: connectionID,
TokenIn: sdk.NewCoin("uatom", math.NewInt(1000)),
TokenOut: "uosmo",
Price: math.LegacyNewDec(10),
Type: OrderTypeLimit,
Status: OrderStatusOpen,
FilledAmount: math.ZeroInt(),
RemainingAmount: math.NewInt(1000),
CreatedAt: ctx.BlockTime().Unix(),
UpdatedAt: ctx.BlockTime().Unix(),
}, nil
}
// GetOrdersByDID retrieves all orders for a DID
func (k Keeper) GetOrdersByDID(
ctx sdk.Context,
did string,
status OrderStatus,
) ([]*OrderInfo, error) {
// This would query orders from state or remote chain
// For now, return empty list
return []*OrderInfo{}, nil
}
// ValidateOrderParameters validates order parameters
func (k Keeper) ValidateOrderParameters(
tokenIn sdk.Coin,
tokenOutDenom string,
price math.LegacyDec,
orderType OrderType,
) error {
if tokenIn.IsZero() {
return fmt.Errorf("token in amount cannot be zero")
}
if tokenOutDenom == "" {
return fmt.Errorf("token out denomination cannot be empty")
}
if tokenIn.Denom == tokenOutDenom {
return fmt.Errorf("cannot create order with same token")
}
if price.IsNegative() || price.IsZero() {
return fmt.Errorf("price must be positive")
}
if orderType < OrderTypeLimit || orderType > OrderTypeTakeProfit {
return fmt.Errorf("invalid order type")
}
return nil
}
+330
View File
@@ -0,0 +1,330 @@
package keeper
import (
"context"
"fmt"
"github.com/sonr-io/sonr/crypto/keys"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/dex/types"
)
// PermissionValidator wraps UCAN verifier for DEX-specific permission validation
type PermissionValidator struct {
verifier *ucan.Verifier
keeper Keeper
permissions *types.UCANPermissionRegistry
}
// NewPermissionValidator creates a new DEX permission validator
func NewPermissionValidator(keeper Keeper) *PermissionValidator {
didResolver := &DEXDIDResolver{keeper: keeper}
verifier := ucan.NewVerifier(didResolver)
return &PermissionValidator{
verifier: verifier,
keeper: keeper,
permissions: types.NewUCANPermissionRegistry(),
}
}
// ValidatePermission validates UCAN token for DEX operation
func (pv *PermissionValidator) ValidatePermission(
ctx context.Context,
tokenString string,
resourceType string,
resourceID string,
operation types.DEXOperation,
) error {
// Get required UCAN capabilities for the operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
return fmt.Errorf("failed to get required UCAN capabilities: %w", err)
}
// Build resource URI for DEX
mapper := types.NewUCANCapabilityMapper()
resourceURI := mapper.CreateDEXResourceURI(resourceType, resourceID)
// Verify UCAN token grants required capabilities
_, err = pv.verifier.VerifyCapability(
ctx,
tokenString,
resourceURI,
capabilities,
)
if err != nil {
return fmt.Errorf("UCAN validation failed: %w", err)
}
return nil
}
// ValidateSwapPermission validates UCAN token for swap operations
func (pv *PermissionValidator) ValidateSwapPermission(
ctx context.Context,
tokenString string,
poolID string,
amount string,
operation types.DEXOperation,
) error {
// Get required UCAN capabilities for the operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
return fmt.Errorf("failed to get required UCAN capabilities: %w", err)
}
// Build pool resource URI
mapper := types.NewUCANCapabilityMapper()
resourceURI := mapper.CreatePoolResourceURI(poolID)
// Verify UCAN token
token, err := pv.verifier.VerifyCapability(
ctx,
tokenString,
resourceURI,
capabilities,
)
if err != nil {
return fmt.Errorf("UCAN validation failed: %w", err)
}
// Additional amount validation
if err := pv.validateAmountConstraint(token, amount); err != nil {
return fmt.Errorf("amount constraint validation failed: %w", err)
}
return nil
}
// ValidateLiquidityPermission validates UCAN token for liquidity operations
func (pv *PermissionValidator) ValidateLiquidityPermission(
ctx context.Context,
tokenString string,
poolID string,
operation types.DEXOperation,
) error {
// Get required UCAN capabilities for the operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
return fmt.Errorf("failed to get required UCAN capabilities: %w", err)
}
// Build pool resource URI
mapper := types.NewUCANCapabilityMapper()
resourceURI := mapper.CreatePoolResourceURI(poolID)
// Verify UCAN token
_, err = pv.verifier.VerifyCapability(
ctx,
tokenString,
resourceURI,
capabilities,
)
if err != nil {
return fmt.Errorf("UCAN validation failed: %w", err)
}
return nil
}
// ValidateOrderPermission validates UCAN token for order operations
func (pv *PermissionValidator) ValidateOrderPermission(
ctx context.Context,
tokenString string,
orderID string,
operation types.DEXOperation,
) error {
// Get required UCAN capabilities for the operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
return fmt.Errorf("failed to get required UCAN capabilities: %w", err)
}
// Build order resource URI
mapper := types.NewUCANCapabilityMapper()
resourceURI := mapper.CreateOrderResourceURI(orderID)
// Verify UCAN token
_, err = pv.verifier.VerifyCapability(
ctx,
tokenString,
resourceURI,
capabilities,
)
if err != nil {
return fmt.Errorf("UCAN validation failed: %w", err)
}
return nil
}
// VerifyDelegationChain validates complete UCAN delegation chain
func (pv *PermissionValidator) VerifyDelegationChain(
ctx context.Context,
tokenString string,
) error {
return pv.verifier.VerifyDelegationChain(ctx, tokenString)
}
// Internal validation methods
// validateAmountConstraint validates amount constraints
func (pv *PermissionValidator) validateAmountConstraint(
token *ucan.Token,
amount string,
) error {
// For now, we'll accept all amounts
// In a real implementation, we'd check against maximum amounts
// specified in the token's attenuations
return nil
}
// validatePoolConstraint validates pool constraints
func (pv *PermissionValidator) validatePoolConstraint(
token *ucan.Token,
poolID string,
) error {
// Check if the token's resource matches the pool
for _, att := range token.Attenuations {
if simpleResource, ok := att.Resource.(*ucan.SimpleResource); ok {
// Check if resource matches pool pattern
if simpleResource.Scheme == "dex" {
expectedValue := fmt.Sprintf("pool:%s", poolID)
if simpleResource.Value == expectedValue || simpleResource.Value == "pool:*" {
return nil
}
}
}
}
return fmt.Errorf("no matching pool attenuation found for pool %s", poolID)
}
// Helper methods
// CreateAttenuation creates a UCAN attenuation for DEX operations
func (pv *PermissionValidator) CreateAttenuation(
actions []string,
resourceType string,
resourceID string,
) ucan.Attenuation {
return pv.permissions.CreateDEXAttenuation(actions, resourceType, resourceID)
}
// CreateAmountLimitedAttenuation creates an amount-limited UCAN attenuation
func (pv *PermissionValidator) CreateAmountLimitedAttenuation(
actions []string,
poolID string,
maxAmount string,
) ucan.Attenuation {
return pv.permissions.CreateAmountLimitedAttenuation(actions, poolID, maxAmount)
}
// CreatePoolRestrictedAttenuation creates a pool-restricted UCAN attenuation
func (pv *PermissionValidator) CreatePoolRestrictedAttenuation(
actions []string,
allowedPools []string,
) ucan.Attenuation {
return pv.permissions.CreatePoolRestrictedAttenuation(actions, allowedPools)
}
// DEXDIDResolver implements ucan.DIDResolver for DEX module
type DEXDIDResolver struct {
keeper Keeper
}
// ResolveDIDKey resolves DID to public key for UCAN verification
func (r *DEXDIDResolver) ResolveDIDKey(ctx context.Context, did string) (keys.DID, error) {
// For DEX module, we need to resolve DIDs from the DID module
// This would require cross-module keeper access
// Check if the DEX keeper has access to DID keeper
if r.keeper.didKeeper != nil {
didDoc, err := r.keeper.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return keys.DID{}, fmt.Errorf("failed to get DID document: %w", err)
}
if didDoc == nil {
return keys.DID{}, fmt.Errorf("DID document not found")
}
// Parse the DID string into a keys.DID
return keys.Parse(did)
}
return keys.DID{}, fmt.Errorf("DID resolver not available in DEX module")
}
// Gasless transaction support
// SupportsGaslessTransaction checks if a UCAN token supports gasless transactions
func (pv *PermissionValidator) SupportsGaslessTransaction(
ctx context.Context,
tokenString string,
poolID string,
operation types.DEXOperation,
) (bool, uint64, error) {
// Parse and verify the token
token, err := pv.verifier.VerifyToken(ctx, tokenString)
if err != nil {
return false, 0, fmt.Errorf("token verification failed: %w", err)
}
mapper := types.NewUCANCapabilityMapper()
resourceURI := mapper.CreatePoolResourceURI(poolID)
// Check each attenuation for gasless support
for _, att := range token.Attenuations {
if att.Resource.GetURI() == resourceURI {
// Check if capability supports gasless transactions
if gaslessCapability, ok := att.Capability.(*ucan.GaslessCapability); ok {
if gaslessCapability.SupportsGasless() {
// Verify the capability grants the required operation
capabilities, err := pv.permissions.GetRequiredUCANCapabilities(operation)
if err != nil {
continue
}
if gaslessCapability.Grants(capabilities) {
return true, gaslessCapability.GetGasLimit(), nil
}
}
}
}
}
return false, 0, nil
}
// ValidateRateLimit checks if a UCAN token has rate limiting and if it's within limits
func (pv *PermissionValidator) ValidateRateLimit(
ctx context.Context,
tokenString string,
poolID string,
) (bool, uint64, uint64, error) {
// Parse and verify the token
token, err := pv.verifier.VerifyToken(ctx, tokenString)
if err != nil {
return false, 0, 0, fmt.Errorf("token verification failed: %w", err)
}
mapper := types.NewUCANCapabilityMapper()
resourceURI := mapper.CreatePoolResourceURI(poolID)
// Check each attenuation for rate limiting
for _, att := range token.Attenuations {
if att.Resource.GetURI() == resourceURI {
// Check if this is a gasless capability with limits
if gaslessCapability, ok := att.Capability.(*ucan.GaslessCapability); ok {
if gaslessCapability.AllowGasless && gaslessCapability.GasLimit > 0 {
// Use gas limit as a proxy for rate limiting
return true, gaslessCapability.GasLimit, 60, nil // 60 second window
}
}
}
}
return false, 0, 0, nil
}
+216
View File
@@ -0,0 +1,216 @@
// Package keeper implements the dex module keeper
package keeper
import (
"fmt"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Portfolio represents a user's portfolio across chains
type Portfolio struct {
DID string
Connections []string
Balances map[string]sdk.Coins // connectionID -> balances
Positions map[string]*Position // positionID -> position
TotalValue math.LegacyDec
UpdatedAt int64
}
// Position represents a liquidity or staking position
type Position struct {
PositionID string
ConnectionID string
PoolID uint64
Type PositionType
Shares math.Int
Value sdk.Coins
APR math.LegacyDec
CreatedAt int64
}
// PositionType represents the type of position
type PositionType int
const (
PositionTypeLiquidity PositionType = iota
PositionTypeStaking
PositionTypeLending
PositionTypeBorrowing
)
// GetPortfolio retrieves the complete portfolio for a DID
func (k Keeper) GetPortfolio(
ctx sdk.Context,
did string,
) (*Portfolio, error) {
// Get all DEX accounts for this DID
accounts, err := k.GetDEXAccountsByDID(ctx, did)
if err != nil {
return nil, fmt.Errorf("failed to get DEX accounts: %w", err)
}
portfolio := &Portfolio{
DID: did,
Connections: make([]string, 0),
Balances: make(map[string]sdk.Coins),
Positions: make(map[string]*Position),
TotalValue: math.LegacyZeroDec(),
UpdatedAt: ctx.BlockTime().Unix(),
}
// Collect connections
for _, account := range accounts {
portfolio.Connections = append(portfolio.Connections, account.ConnectionId)
// Get balances for each connection
balances, err := k.GetRemoteBalances(ctx, did, account.ConnectionId)
if err == nil {
portfolio.Balances[account.ConnectionId] = balances
}
}
// Calculate total value (simplified - would need price feeds)
portfolio.TotalValue = k.CalculatePortfolioValue(ctx, portfolio.Balances)
return portfolio, nil
}
// GetRemoteBalances queries balances on a remote chain
func (k Keeper) GetRemoteBalances(
ctx sdk.Context,
did string,
connectionID string,
) (sdk.Coins, error) {
// This would query the remote chain for balances
// For now, return placeholder balances
return sdk.NewCoins(
sdk.NewCoin("uatom", math.NewInt(1000000)),
sdk.NewCoin("uosmo", math.NewInt(2000000)),
), nil
}
// GetPositions retrieves all positions for a DID
func (k Keeper) GetPositions(
ctx sdk.Context,
did string,
connectionID string,
) ([]*Position, error) {
// This would query positions from remote chain
// For now, return empty list
return []*Position{}, nil
}
// CalculatePortfolioValue calculates the total portfolio value
func (k Keeper) CalculatePortfolioValue(
ctx sdk.Context,
balances map[string]sdk.Coins,
) math.LegacyDec {
// This would use price feeds to calculate USD value
// For now, return a simple sum of amounts
totalValue := math.LegacyZeroDec()
for _, coins := range balances {
for _, coin := range coins {
// Simplified: assume 1:1 USD value
totalValue = totalValue.Add(math.LegacyNewDecFromInt(coin.Amount))
}
}
return totalValue
}
// GetPortfolioHistory retrieves historical portfolio data
func (k Keeper) GetPortfolioHistory(
ctx sdk.Context,
did string,
startTime int64,
endTime int64,
) ([]*PortfolioSnapshot, error) {
// This would retrieve historical snapshots from state
// For now, return empty list
return []*PortfolioSnapshot{}, nil
}
// PortfolioSnapshot represents a point-in-time portfolio state
type PortfolioSnapshot struct {
Timestamp int64
TotalValue math.LegacyDec
Balances map[string]sdk.Coins
Positions int
}
// UpdatePortfolioSnapshot creates a new portfolio snapshot
func (k Keeper) UpdatePortfolioSnapshot(
ctx sdk.Context,
did string,
) error {
portfolio, err := k.GetPortfolio(ctx, did)
if err != nil {
return fmt.Errorf("failed to get portfolio: %w", err)
}
snapshot := &PortfolioSnapshot{
Timestamp: ctx.BlockTime().Unix(),
TotalValue: portfolio.TotalValue,
Balances: portfolio.Balances,
Positions: len(portfolio.Positions),
}
// Store snapshot in state or DWN
// Implementation would depend on storage strategy
_ = snapshot
return nil
}
// GetPortfolioPerformance calculates portfolio performance metrics
func (k Keeper) GetPortfolioPerformance(
ctx sdk.Context,
did string,
period int64, // Period in seconds
) (*PerformanceMetrics, error) {
// This would calculate performance based on historical data
// For now, return placeholder metrics
return &PerformanceMetrics{
TotalReturn: math.LegacyNewDec(10), // 10% return
TotalReturnPct: math.LegacyNewDecWithPrec(10, 2), // 10%
DailyReturn: math.LegacyNewDec(1), // 1% daily
APY: math.LegacyNewDecWithPrec(365, 2), // 365% APY (simplified)
Volatility: math.LegacyNewDecWithPrec(15, 2), // 15% volatility
SharpeRatio: math.LegacyNewDecWithPrec(2, 1), // 2.0 Sharpe
MaxDrawdown: math.LegacyNewDecWithPrec(5, 2), // 5% max drawdown
}, nil
}
// PerformanceMetrics represents portfolio performance metrics
type PerformanceMetrics struct {
TotalReturn math.LegacyDec
TotalReturnPct math.LegacyDec
DailyReturn math.LegacyDec
APY math.LegacyDec
Volatility math.LegacyDec
SharpeRatio math.LegacyDec
MaxDrawdown math.LegacyDec
}
// GetTopPerformers returns the top performing assets in portfolio
func (k Keeper) GetTopPerformers(
ctx sdk.Context,
did string,
limit int,
) ([]*AssetPerformance, error) {
// This would analyze asset performance
// For now, return empty list
return []*AssetPerformance{}, nil
}
// AssetPerformance represents performance of a single asset
type AssetPerformance struct {
Asset string
Connection string
Return math.LegacyDec
ReturnPct math.LegacyDec
Volume math.Int
}
+174
View File
@@ -0,0 +1,174 @@
package keeper
import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/sonr-io/sonr/x/dex/types"
)
var _ types.QueryServer = queryServer{}
type queryServer struct {
Keeper
}
// NewQueryServerImpl returns an implementation of the module QueryServer.
func NewQueryServerImpl(k Keeper) types.QueryServer {
return queryServer{Keeper: k}
}
// Params queries the module parameters.
func (qs queryServer) Params(ctx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
params, err := qs.Keeper.Params.Get(sdkCtx)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &types.QueryParamsResponse{Params: params}, nil
}
// Account queries a specific DEX account.
func (qs queryServer) Account(ctx context.Context, req *types.QueryAccountRequest) (*types.QueryAccountResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
if req.Did == "" || req.ConnectionId == "" {
return nil, status.Error(codes.InvalidArgument, "did and connection_id are required")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
account, err := qs.Keeper.GetDEXAccount(sdkCtx, req.Did, req.ConnectionId)
if err != nil {
return nil, status.Error(codes.NotFound, err.Error())
}
return &types.QueryAccountResponse{Account: account}, nil
}
// Accounts queries all DEX accounts for a specific DID.
func (qs queryServer) Accounts(ctx context.Context, req *types.QueryAccountsRequest) (*types.QueryAccountsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
if req.Did == "" {
return nil, status.Error(codes.InvalidArgument, "did is required")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
accounts, err := qs.Keeper.GetDEXAccountsByDID(sdkCtx, req.Did)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
// Convert to pointer slice for response
accountPtrs := make([]*types.InterchainDEXAccount, len(accounts))
for i := range accounts {
accountPtrs[i] = &accounts[i]
}
return &types.QueryAccountsResponse{Accounts: accountPtrs}, nil
}
// TODO: Balance - Implement cross-chain balance query via IBC
// This method should query token balances on remote chains through IBC queries
// Required implementation steps:
// 1. Validate request parameters (DID, connection ID, denoms)
// 2. Retrieve the ICA account address for this DID and connection
// 3. Construct IBC query packet for bank balance on remote chain
// 4. Send IBC query through the appropriate channel
// 5. Parse the response and convert remote denoms to local representation
// 6. Cache balance data temporarily for performance optimization
// Returns: List of coin balances on the remote chain
// Balance queries remote chain balance.
func (qs queryServer) Balance(ctx context.Context, req *types.QueryBalanceRequest) (*types.QueryBalanceResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
// TODO: Implement balance query via ICA
// This would require querying the remote chain through IBC
return &types.QueryBalanceResponse{
Balances: sdk.NewCoins(),
}, nil
}
// TODO: Pool - Implement cross-chain liquidity pool query via IBC
// This method should query pool information from remote DEX protocols
// Required implementation steps:
// 1. Validate request parameters (pool ID, connection ID)
// 2. Construct IBC query packet for pool state on remote DEX
// 3. Send IBC query through the appropriate channel
// 4. Parse pool data including reserves, total shares, and fee parameters
// 5. Calculate derived metrics (price, APY, volume) if available
// 6. Cache pool data with appropriate TTL for performance
// Returns: Pool reserves, LP token supply, fee rate, and current price
// Pool queries pool information.
func (qs queryServer) Pool(ctx context.Context, req *types.QueryPoolRequest) (*types.QueryPoolResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
// TODO: Implement pool query via ICA
// This would require querying the remote chain through IBC
return &types.QueryPoolResponse{}, nil
}
// TODO: Orders - Implement order book query for user's limit orders
// This method should retrieve all orders for a specific DID across connections
// Required implementation steps:
// 1. Validate request parameters (DID, optional status filter)
// 2. Query local state for stored order records by DID
// 3. Filter orders by status (open, filled, cancelled) if specified
// 4. For open orders, optionally query remote chain for current status
// 5. Sort orders by creation time or specified sort parameter
// 6. Apply pagination if limits are provided
// 7. Include order fills and partial fill information
// Returns: List of orders with status, amounts, prices, and timestamps
// Orders queries orders for a DID.
func (qs queryServer) Orders(ctx context.Context, req *types.QueryOrdersRequest) (*types.QueryOrdersResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
// TODO: Implement orders query
// This would require storing order information in state or DWN
return &types.QueryOrdersResponse{
Orders: []*types.Order{}, // Empty for now
}, nil
}
// TODO: History - Implement transaction history query from DWN storage
// This method should retrieve complete transaction history for a DID
// Required implementation steps:
// 1. Validate request parameters (DID, time range, transaction type filter)
// 2. Query DWN for stored transaction records using DID as key
// 3. Filter transactions by type (swap, liquidity, order) if specified
// 4. Apply time range filter for date-based queries
// 5. Calculate profit/loss metrics for each transaction
// 6. Include gas costs and fees in transaction details
// 7. Sort by timestamp (newest first by default)
// 8. Apply pagination with cursor-based navigation
// Returns: List of transactions with full details and pagination info
// History queries transaction history.
func (qs queryServer) History(ctx context.Context, req *types.QueryHistoryRequest) (*types.QueryHistoryResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
// TODO: Implement history query
// This would require storing transaction history in state or DWN
return &types.QueryHistoryResponse{
Transactions: []*types.Transaction{}, // Empty for now
}, nil
}
+125
View File
@@ -0,0 +1,125 @@
package keeper
import (
"fmt"
"time"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/sonr-io/sonr/x/dex/types"
)
// ExecuteSwap handles swap execution through ICA
func (k Keeper) ExecuteSwap(
ctx sdk.Context,
did string,
connectionID string,
tokenIn sdk.Coin,
tokenOutDenom string,
minAmountOut math.Int,
poolID uint64,
) (uint64, error) {
// Get the DEX account
account, err := k.GetDEXAccount(ctx, did, connectionID)
if err != nil {
return 0, fmt.Errorf("DEX account not found: %w", err)
}
// Verify account is active
if account.Status != types.ACCOUNT_STATUS_ACTIVE {
return 0, fmt.Errorf("DEX account is not active")
}
// Create swap message for remote chain
// This example uses a generic bank send as placeholder
// Actual implementation would use chain-specific swap messages
swapMsg := &banktypes.MsgSend{
FromAddress: account.AccountAddress,
ToAddress: account.AccountAddress, // Swap to self as example
Amount: sdk.NewCoins(tokenIn),
}
// Send the swap transaction via ICA
sequence, err := k.SendDEXTransaction(
ctx,
did,
connectionID,
[]sdk.Msg{swapMsg},
fmt.Sprintf("swap_%s_for_%s", tokenIn.Denom, tokenOutDenom),
30*time.Second,
)
if err != nil {
return 0, fmt.Errorf("failed to send swap transaction: %w", err)
}
// Emit swap event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSwapExecuted,
sdk.NewAttribute("did", did),
sdk.NewAttribute("connection", connectionID),
sdk.NewAttribute("token_in", tokenIn.String()),
sdk.NewAttribute("token_out_denom", tokenOutDenom),
sdk.NewAttribute("sequence", fmt.Sprintf("%d", sequence)),
),
)
return sequence, nil
}
// BuildOsmosisSwapMsg builds an Osmosis-specific swap message
func (k Keeper) BuildOsmosisSwapMsg(
senderAddress string,
poolID uint64,
tokenIn sdk.Coin,
tokenOutDenom string,
minAmountOut math.Int,
) sdk.Msg {
// This would build an actual Osmosis swap message
// For now, return a placeholder bank send
return &banktypes.MsgSend{
FromAddress: senderAddress,
ToAddress: senderAddress,
Amount: sdk.NewCoins(tokenIn),
}
}
// EstimateSwapOutput estimates the output of a swap
func (k Keeper) EstimateSwapOutput(
ctx sdk.Context,
connectionID string,
poolID uint64,
tokenIn sdk.Coin,
tokenOutDenom string,
) (math.Int, error) {
// This would query the remote chain for swap estimation
// For now, return a placeholder value
return tokenIn.Amount.MulRaw(95).QuoRaw(100), nil // 95% of input as example
}
// ValidateSwapParameters validates swap parameters
func (k Keeper) ValidateSwapParameters(
tokenIn sdk.Coin,
tokenOutDenom string,
minAmountOut math.Int,
) error {
if tokenIn.IsZero() {
return fmt.Errorf("token in amount cannot be zero")
}
if tokenOutDenom == "" {
return fmt.Errorf("token out denomination cannot be empty")
}
if tokenIn.Denom == tokenOutDenom {
return fmt.Errorf("cannot swap same token")
}
if minAmountOut.IsNegative() {
return fmt.Errorf("minimum amount out cannot be negative")
}
return nil
}
+210
View File
@@ -0,0 +1,210 @@
// Package keeper implements UCAN integration for the DEX module
package keeper
import (
"encoding/json"
"fmt"
"slices"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/x/dex/types"
)
// ValidateUCANForDEXOperation validates UCAN token for a DEX operation
func (k Keeper) ValidateUCANForDEXOperation(
ctx sdk.Context,
ucanToken string,
did string,
operation string,
params map[string]any,
) error {
if ucanToken == "" {
// No UCAN provided - check if operation requires it
if k.requiresUCAN(operation) {
return fmt.Errorf("UCAN token required for operation %s", operation)
}
return nil
}
// Validate UCAN token structure and signature
capability, err := k.parseUCANToken(ucanToken)
if err != nil {
return fmt.Errorf("invalid UCAN token: %w", err)
}
// Check expiration
if ctx.BlockTime().After(capability.Expiration) {
return fmt.Errorf("UCAN token expired")
}
// Verify resource matches operation
expectedResource := k.getResourceForOperation(operation)
if !k.resourceMatches(capability.Resource, expectedResource) {
return fmt.Errorf(
"UCAN resource %s does not match operation %s",
capability.Resource,
operation,
)
}
// Verify ability
if !k.hasAbility(capability.Ability, operation) {
return fmt.Errorf(
"UCAN ability %s insufficient for operation %s",
capability.Ability,
operation,
)
}
// Validate constraints
if err := k.validateConstraints(capability.Constraints, params); err != nil {
return fmt.Errorf("UCAN constraints not satisfied: %w", err)
}
return nil
}
// requiresUCAN checks if an operation requires UCAN authorization
func (k Keeper) requiresUCAN(operation string) bool {
// Critical operations that always require UCAN
criticalOps := []string{
"large_swap", // Swaps above threshold
"remove_liquidity", // Removing liquidity
"cancel_all_orders", // Canceling all orders
}
return slices.Contains(criticalOps, operation)
}
// parseUCANToken parses and validates a UCAN token
func (k Keeper) parseUCANToken(token string) (*types.UCANCapability, error) {
// This is a simplified implementation
// Real implementation would validate JWT signature and parse claims
// For now, parse as JSON for simplicity
var capability types.UCANCapability
if err := json.Unmarshal([]byte(token), &capability); err != nil {
return nil, fmt.Errorf("failed to parse UCAN token: %w", err)
}
return &capability, nil
}
// getResourceForOperation maps operations to UCAN resources
func (k Keeper) getResourceForOperation(operation string) string {
resourceMap := map[string]string{
"swap": "dex:swap",
"execute_swap": "dex:swap",
"provide_liquidity": "dex:liquidity:provide",
"remove_liquidity": "dex:liquidity:remove",
"create_order": "dex:order:create",
"cancel_order": "dex:order:cancel",
"register_account": "dex:account:register",
}
if resource, ok := resourceMap[operation]; ok {
return resource
}
return fmt.Sprintf("dex:%s", operation)
}
// resourceMatches checks if UCAN resource matches required resource
func (k Keeper) resourceMatches(ucanResource, requiredResource string) bool {
// Exact match
if ucanResource == requiredResource {
return true
}
// Wildcard match (e.g., "dex:*" matches any DEX operation)
if strings.HasSuffix(ucanResource, ":*") {
prefix := strings.TrimSuffix(ucanResource, "*")
return strings.HasPrefix(requiredResource, prefix)
}
// Hierarchical match (e.g., "dex:swap" matches "dex:swap:osmosis")
return strings.HasPrefix(requiredResource, ucanResource+":")
}
// hasAbility checks if UCAN ability is sufficient for operation
func (k Keeper) hasAbility(ucanAbility, operation string) bool {
// Map operations to required abilities
requiredAbilities := map[string][]string{
"swap": {"execute", "trade"},
"provide_liquidity": {"execute", "provide"},
"remove_liquidity": {"execute", "remove"},
"create_order": {"execute", "create"},
"cancel_order": {"execute", "cancel"},
"read": {"read", "view"},
}
required, ok := requiredAbilities[operation]
if !ok {
// Default to requiring "execute" ability
required = []string{"execute"}
}
// Check if UCAN ability matches any required ability
for _, req := range required {
if ucanAbility == req || ucanAbility == "*" {
return true
}
}
return false
}
// validateConstraints validates UCAN constraints against operation parameters
func (k Keeper) validateConstraints(constraints, params map[string]any) error {
// Check amount constraints
if maxAmount, ok := constraints["max_amount"]; ok {
if amount, ok := params["amount"]; ok {
if !k.isAmountWithinLimit(amount, maxAmount) {
return fmt.Errorf("amount exceeds UCAN limit")
}
}
}
// Check pool constraints
if allowedPools, ok := constraints["allowed_pools"]; ok {
if poolID, ok := params["pool_id"]; ok {
if !k.isPoolAllowed(poolID, allowedPools) {
return fmt.Errorf("pool not allowed by UCAN")
}
}
}
// Check chain constraints
if allowedChains, ok := constraints["allowed_chains"]; ok {
if connectionID, ok := params["connection_id"]; ok {
if !k.isChainAllowed(connectionID, allowedChains) {
return fmt.Errorf("chain not allowed by UCAN")
}
}
}
return nil
}
// isAmountWithinLimit checks if amount is within UCAN limit
func (k Keeper) isAmountWithinLimit(amount, maxAmount any) bool {
// Convert and compare amounts
// Simplified implementation - real one would handle different types
return true
}
// isPoolAllowed checks if pool is in allowed list
func (k Keeper) isPoolAllowed(poolID, allowedPools any) bool {
// Check if pool is in allowed list
// Simplified implementation
return true
}
// isChainAllowed checks if chain is in allowed list
func (k Keeper) isChainAllowed(connectionID, allowedChains any) bool {
// Check if chain connection is allowed
// Simplified implementation
return true
}