mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Executable
+84
@@ -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
|
||||
}
|
||||
Executable
+103
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Executable
+181
@@ -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)
|
||||
}
|
||||
@@ -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{}
|
||||
@@ -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
|
||||
}
|
||||
Executable
+219
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user