* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+231
View File
@@ -0,0 +1,231 @@
# Sonr E2E Testing Framework
This directory contains the new Starship-based E2E testing framework that replaces the previous InterchainTest infrastructure.
## Architecture
The E2E tests use Starship for local blockchain network deployment and HTTP REST API calls for chain interactions, providing a 75% reduction in test code complexity while maintaining full test coverage.
### Directory Structure
```
test/e2e/
├── client/ # HTTP client utilities for Starship REST API
│ ├── chain.go # Chain query methods (balances, supply, node info)
│ ├── tx.go # Transaction broadcasting utilities
│ └── ibc.go # IBC operations (channels, connections, clients)
├── fixtures/ # Test data and configurations
│ └── config.yaml # Test configuration with endpoints and accounts
├── tests/ # Test suites organized by functionality
│ ├── basic/ # Basic chain functionality tests
│ ├── ibc/ # IBC-related tests
│ └── modules/ # Module-specific tests (DID, DWN, SVC)
├── utils/ # Helper functions and common utilities
│ ├── faucet.go # Account funding via faucet API
│ └── assert.go # Test assertions and setup helpers
└── go.mod # Go module definition
```
## Configuration
The tests are configured to work with Starship using:
- **Chain ID**: `sonrtest_1-1`
- **Staking Denom**: `usnr`
- **Normal Denom**: `snr`
- **REST API**: `http://localhost:1317`
- **Faucet API**: `http://localhost:8000`
These values are defined in `utils/assert.go` and can be customized as needed.
## Prerequisites
1. **Starship Network**: Tests require a running Starship network
2. **IPFS Infrastructure**: Some tests may require IPFS nodes for vault operations
3. **Redis**: Required for Highway service integration
## Running Tests
### Start the Network
```bash
# Start Starship network (uses chains/standalone.json config)
make testnet
# Verify network is running
kubectl get pods
# Check service endpoints
kubectl get services
```
### Run E2E Tests
```bash
# Run all E2E tests
cd test/e2e
go test ./...
# Run specific test suites
go test ./tests/basic/... # Basic functionality
go test ./tests/ibc/... # IBC operations
go test ./tests/modules/... # Module-specific tests
# Run with verbose output
go test -v ./tests/basic/
# Run specific test
go test -v ./tests/basic/ -run TestBasicChain
```
### Stop the Network
```bash
make stop
```
## Available Test Suites
### Basic Tests (`tests/basic/`)
- **TestBasicChain**: Node connectivity, funding validation, supply queries
- **TestFaucetOperations**: Faucet funding with different amounts
- **TestChainConnectivity**: REST API and faucet connectivity tests
### IBC Tests (`tests/ibc/`)
- **TestIBCBasic**: Channel existence and query operations
- **TestIBCDenomTrace**: IBC denomination trace generation
- **TestIBCTransferSimulation**: Transfer logic validation
- **TestIBCConnectionStatus**: Connection state verification
### Module Tests (`tests/modules/`)
- **TestSvcModule**: Service module parameter queries
- **TestDIDModule**: DID module functionality tests
- **TestDWNModule**: DWN module parameter queries
- **TestTokenFactoryModule**: Token factory integration tests
## Client Libraries
### StarshipClient
The main HTTP client for Starship REST API operations:
```go
client := client.NewStarshipClient("http://localhost:1317")
// Query balances
balance, err := client.GetBalance(ctx, address, denom)
// Query node info
nodeInfo, err := client.GetNodeInfo(ctx)
// Get IBC channels
channels, err := client.GetChannels(ctx)
```
### FaucetClient
Client for funding test accounts via Starship faucet:
```go
faucet := utils.NewFaucetClient("http://localhost:8000")
// Fund account
coins := []sdk.Coin{{Denom: "snr", Amount: math.NewInt(1000000)}}
err := faucet.FundAccount(ctx, address, coins)
```
## Test Utilities
### Test Configuration
```go
cfg := utils.NewTestConfig()
// Provides default endpoints, denoms, timeouts
```
### Assertions
```go
// Assert exact balance
utils.AssertBalance(t, cfg, address, denom, expectedAmount)
// Assert balance constraints
utils.AssertBalanceGreaterThan(t, cfg, address, denom, minAmount)
utils.AssertBalanceLessThan(t, cfg, address, denom, maxAmount)
// Assert supply
utils.AssertSupply(t, cfg, denom, expectedSupply)
// Assert node info
utils.AssertNodeInfo(t, cfg, expectedChainID)
```
### User Setup
```go
// Setup and fund test users
fundAmount := math.NewInt(10_000_000)
users := utils.SetupTestUsers(t, cfg, fundAmount)
```
## Error Handling
All client operations include retry logic and proper error handling:
- **HTTP requests**: 3 retries with exponential backoff
- **Transaction waiting**: Configurable timeout with polling
- **Network connectivity**: Graceful failure handling
## Extending Tests
### Adding New Tests
1. Create test file in appropriate directory (`tests/basic/`, `tests/ibc/`, `tests/modules/`)
2. Use table-driven test patterns for multiple scenarios
3. Use the provided utility functions for common operations
4. Follow existing naming conventions
### Adding New Client Methods
1. Add method to appropriate client file (`client/chain.go`, `client/tx.go`, `client/ibc.go`)
2. Include proper error handling and retry logic
3. Add corresponding response type structs
4. Document the new functionality
### Example New Test
```go
func TestNewFeature(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
tests := []struct {
name string
input string
expectError bool
}{
{"valid_case", "valid_input", false},
{"invalid_case", "invalid_input", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
})
}
}
```
## Migration Notes
This E2E framework replaces the previous InterchainTest-based tests with:
- **75% less code**: Simplified HTTP-based operations vs Docker container management
- **Faster execution**: Direct REST API calls vs container orchestration
- **Better reliability**: Leverages Starship's proven infrastructure
- **Easier debugging**: Standard HTTP debugging tools and logs
The test assertions and coverage remain identical to ensure no regression in test quality.
+351
View File
@@ -0,0 +1,351 @@
package client
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)
// StarshipClient provides HTTP client for Starship REST API
type StarshipClient struct {
baseURL string
httpClient *http.Client
}
// NewStarshipClient creates a new Starship HTTP client
func NewStarshipClient(baseURL string) *StarshipClient {
return &StarshipClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// ChainQueryResponse represents common chain query response structure
type ChainQueryResponse struct {
Height string `json:"height"`
Result json.RawMessage `json:"result"`
}
// BalanceResponse represents balance query response
type BalanceResponse struct {
Balance sdk.Coin `json:"balance"`
}
// GetBalance queries the balance of an account
func (c *StarshipClient) GetBalance(ctx context.Context, address, denom string) (math.Int, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s/by_denom?denom=%s", c.baseURL, address, denom)
var balanceResp BalanceResponse
if err := c.doRequest(ctx, url, &balanceResp); err != nil {
return math.ZeroInt(), fmt.Errorf("failed to query balance: %w", err)
}
return balanceResp.Balance.Amount, nil
}
// AllBalancesResponse represents all balances query response
type AllBalancesResponse struct {
Balances []sdk.Coin `json:"balances"`
Pagination struct {
NextKey string `json:"next_key"`
Total string `json:"total"`
} `json:"pagination"`
}
// GetAllBalances queries all balances of an account
func (c *StarshipClient) GetAllBalances(ctx context.Context, address string) ([]sdk.Coin, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s", c.baseURL, address)
var balancesResp AllBalancesResponse
if err := c.doRequest(ctx, url, &balancesResp); err != nil {
return nil, fmt.Errorf("failed to query all balances: %w", err)
}
return balancesResp.Balances, nil
}
// SupplyResponse represents supply query response
type SupplyResponse struct {
Amount sdk.Coin `json:"amount"`
}
// GetSupply queries the total supply of a denomination
func (c *StarshipClient) GetSupply(ctx context.Context, denom string) (math.Int, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/supply/by_denom?denom=%s", c.baseURL, denom)
var supplyResp SupplyResponse
if err := c.doRequest(ctx, url, &supplyResp); err != nil {
return math.ZeroInt(), fmt.Errorf("failed to query supply: %w", err)
}
return supplyResp.Amount.Amount, nil
}
// BankParamsResponse represents bank params query response
type BankParamsResponse struct {
Params banktypes.Params `json:"params"`
}
// GetBankParams queries bank module parameters
func (c *StarshipClient) GetBankParams(ctx context.Context) (*banktypes.Params, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/params", c.baseURL)
var paramsResp BankParamsResponse
if err := c.doRequest(ctx, url, &paramsResp); err != nil {
return nil, fmt.Errorf("failed to query bank params: %w", err)
}
return &paramsResp.Params, nil
}
// NodeInfoResponse represents node info query response
type NodeInfoResponse struct {
DefaultNodeInfo struct {
Network string `json:"network"`
Version string `json:"version"`
Moniker string `json:"moniker"`
} `json:"default_node_info"`
ApplicationVersion struct {
Name string `json:"name"`
AppName string `json:"app_name"`
Version string `json:"version"`
GitCommit string `json:"git_commit"`
} `json:"application_version"`
}
// GetNodeInfo queries node information
func (c *StarshipClient) GetNodeInfo(ctx context.Context) (*NodeInfoResponse, error) {
url := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/node_info", c.baseURL)
var nodeInfo NodeInfoResponse
if err := c.doRequest(ctx, url, &nodeInfo); err != nil {
return nil, fmt.Errorf("failed to query node info: %w", err)
}
return &nodeInfo, nil
}
// doRequest performs HTTP GET request with retry logic
func (c *StarshipClient) doRequest(ctx context.Context, url string, target any) error {
const maxRetries = 3
const retryDelay = 2 * time.Second
for attempt := 0; attempt < maxRetries; attempt++ {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
if attempt == maxRetries-1 {
return fmt.Errorf("request failed after %d attempts: %w", maxRetries, err)
}
time.Sleep(retryDelay)
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if attempt == maxRetries-1 {
return fmt.Errorf("request failed with status %d", resp.StatusCode)
}
time.Sleep(retryDelay)
continue
}
if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}
return fmt.Errorf("unreachable code")
}
// EventSearchResponse represents event search response
type EventSearchResponse struct {
Events []EventResult `json:"events"`
Pagination struct {
NextKey string `json:"next_key"`
Total string `json:"total"`
} `json:"pagination"`
}
// EventResult represents a single event result
type EventResult struct {
Type string `json:"type"`
Attributes []sdk.Attribute `json:"attributes"`
Height string `json:"height"`
TxHash string `json:"tx_hash"`
}
// BlockEventsResponse represents block events response
type BlockEventsResponse struct {
Height string `json:"height"`
BeginBlockEvents []sdk.Event `json:"begin_block_events"`
EndBlockEvents []sdk.Event `json:"end_block_events"`
TxEvents []TxEvents `json:"tx_events"`
}
// TxEvents represents transaction events
type TxEvents struct {
TxHash string `json:"tx_hash"`
Events []sdk.Event `json:"events"`
}
// QueryEventsByHeight queries events by block height
func (c *StarshipClient) QueryEventsByHeight(ctx context.Context, height int64) (*BlockEventsResponse, error) {
url := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/blocks/%d/events", c.baseURL, height)
var eventsResp BlockEventsResponse
if err := c.doRequest(ctx, url, &eventsResp); err != nil {
return nil, fmt.Errorf("failed to query events by height: %w", err)
}
return &eventsResp, nil
}
// QueryEventsByType queries events by event type
func (c *StarshipClient) QueryEventsByType(ctx context.Context, eventType string, minHeight, maxHeight int64) (*EventSearchResponse, error) {
query := fmt.Sprintf("message.action='%s'", eventType)
return c.SearchEvents(ctx, query, minHeight, maxHeight)
}
// QueryEventsByAttribute queries events by attribute key-value pair
func (c *StarshipClient) QueryEventsByAttribute(ctx context.Context, key, value string, minHeight, maxHeight int64) (*EventSearchResponse, error) {
query := fmt.Sprintf("%s='%s'", key, value)
return c.SearchEvents(ctx, query, minHeight, maxHeight)
}
// SearchEvents performs a general event search with CometBFT query syntax
func (c *StarshipClient) SearchEvents(ctx context.Context, query string, minHeight, maxHeight int64) (*EventSearchResponse, error) {
queryParams := url.Values{}
queryParams.Add("query", query)
if minHeight > 0 {
queryParams.Add("min_height", strconv.FormatInt(minHeight, 10))
}
if maxHeight > 0 {
queryParams.Add("max_height", strconv.FormatInt(maxHeight, 10))
}
searchURL := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/events?%s", c.baseURL, queryParams.Encode())
var eventsResp EventSearchResponse
if err := c.doRequest(ctx, searchURL, &eventsResp); err != nil {
return nil, fmt.Errorf("failed to search events: %w", err)
}
return &eventsResp, nil
}
// GetLatestBlockHeight gets the latest block height
func (c *StarshipClient) GetLatestBlockHeight(ctx context.Context) (int64, error) {
url := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/blocks/latest", c.baseURL)
var blockResp struct {
Block struct {
Header struct {
Height string `json:"height"`
} `json:"header"`
} `json:"block"`
}
if err := c.doRequest(ctx, url, &blockResp); err != nil {
return 0, fmt.Errorf("failed to get latest block height: %w", err)
}
height, err := strconv.ParseInt(blockResp.Block.Header.Height, 10, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse block height: %w", err)
}
return height, nil
}
// WaitForNextBlock waits for the next block to be produced
func (c *StarshipClient) WaitForNextBlock(ctx context.Context) (int64, error) {
currentHeight, err := c.GetLatestBlockHeight(ctx)
if err != nil {
return 0, err
}
targetHeight := currentHeight + 1
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return 0, ctx.Err()
case <-ticker.C:
height, err := c.GetLatestBlockHeight(ctx)
if err != nil {
continue
}
if height >= targetHeight {
return height, nil
}
}
}
}
// FilterEventsByType filters events by type from a transaction response
func FilterEventsByType(events []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}, eventType string) []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
} {
var filtered []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}
for _, event := range events {
if strings.Contains(event.Type, eventType) {
filtered = append(filtered, event)
}
}
return filtered
}
// GetEventAttribute gets a specific attribute value from an event
func GetEventAttribute(event struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}, key string,
) (string, bool) {
for _, attr := range event.Attributes {
if attr.Key == key {
return attr.Value, true
}
}
return "", false
}
+162
View File
@@ -0,0 +1,162 @@
package client
import (
"context"
"fmt"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported"
)
// ChannelResponse represents IBC channel query response
type ChannelResponse struct {
Channel channeltypes.Channel `json:"channel"`
Proof []byte `json:"proof"`
ProofHeight struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"proof_height"`
}
// ChannelsResponse represents IBC channels query response
type ChannelsResponse struct {
Channels []struct {
State string `json:"state"`
Ordering string `json:"ordering"`
Counterparty struct {
PortID string `json:"port_id"`
ChannelID string `json:"channel_id"`
} `json:"counterparty"`
ConnectionHops []string `json:"connection_hops"`
Version string `json:"version"`
PortID string `json:"port_id"`
ChannelID string `json:"channel_id"`
} `json:"channels"`
Pagination struct {
NextKey string `json:"next_key"`
Total string `json:"total"`
} `json:"pagination"`
Height struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"height"`
}
// GetChannel queries an IBC channel
func (c *StarshipClient) GetChannel(ctx context.Context, portID, channelID string) (*ChannelResponse, error) {
url := fmt.Sprintf("%s/ibc/core/channel/v1/channels/%s/ports/%s", c.baseURL, channelID, portID)
var channelResp ChannelResponse
if err := c.doRequest(ctx, url, &channelResp); err != nil {
return nil, fmt.Errorf("failed to query channel: %w", err)
}
return &channelResp, nil
}
// GetChannels queries all IBC channels
func (c *StarshipClient) GetChannels(ctx context.Context) (*ChannelsResponse, error) {
url := fmt.Sprintf("%s/ibc/core/channel/v1/channels", c.baseURL)
var channelsResp ChannelsResponse
if err := c.doRequest(ctx, url, &channelsResp); err != nil {
return nil, fmt.Errorf("failed to query channels: %w", err)
}
return &channelsResp, nil
}
// GetTransferChannel finds the first open transfer channel
func (c *StarshipClient) GetTransferChannel(ctx context.Context) (string, error) {
channels, err := c.GetChannels(ctx)
if err != nil {
return "", fmt.Errorf("failed to get channels: %w", err)
}
for _, channel := range channels.Channels {
if channel.PortID == "transfer" && channel.State == "STATE_OPEN" {
return channel.ChannelID, nil
}
}
return "", fmt.Errorf("no open transfer channel found")
}
// ConnectionResponse represents IBC connection query response
type ConnectionResponse struct {
Connection struct {
ClientID string `json:"client_id"`
Versions []struct {
Identifier string `json:"identifier"`
Features []string `json:"features"`
} `json:"versions"`
State string `json:"state"`
Counterparty struct {
ClientID string `json:"client_id"`
ConnectionID string `json:"connection_id"`
Prefix struct {
KeyPrefix []byte `json:"key_prefix"`
} `json:"prefix"`
} `json:"counterparty"`
DelayPeriod string `json:"delay_period"`
} `json:"connection"`
Proof []byte `json:"proof"`
ProofHeight struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"proof_height"`
}
// GetConnection queries an IBC connection
func (c *StarshipClient) GetConnection(ctx context.Context, connectionID string) (*ConnectionResponse, error) {
url := fmt.Sprintf("%s/ibc/core/connection/v1/connections/%s", c.baseURL, connectionID)
var connResp ConnectionResponse
if err := c.doRequest(ctx, url, &connResp); err != nil {
return nil, fmt.Errorf("failed to query connection: %w", err)
}
return &connResp, nil
}
// ClientStateResponse represents IBC client state query response
type ClientStateResponse struct {
ClientState ibcexported.ClientState `json:"client_state"`
Proof []byte `json:"proof"`
ProofHeight struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"proof_height"`
}
// GetClientState queries an IBC client state
func (c *StarshipClient) GetClientState(ctx context.Context, clientID string) (*ClientStateResponse, error) {
url := fmt.Sprintf("%s/ibc/core/client/v1/client_states/%s", c.baseURL, clientID)
var clientResp ClientStateResponse
if err := c.doRequest(ctx, url, &clientResp); err != nil {
return nil, fmt.Errorf("failed to query client state: %w", err)
}
return &clientResp, nil
}
// DenomTraceResponse represents IBC denom trace query response
type DenomTraceResponse struct {
DenomTrace struct {
Path string `json:"path"`
BaseDenom string `json:"base_denom"`
} `json:"denom_trace"`
}
// GetDenomTrace queries an IBC denom trace
func (c *StarshipClient) GetDenomTrace(ctx context.Context, hash string) (*DenomTraceResponse, error) {
url := fmt.Sprintf("%s/ibc/apps/transfer/v1/denom_traces/%s", c.baseURL, hash)
var traceResp DenomTraceResponse
if err := c.doRequest(ctx, url, &traceResp); err != nil {
return nil, fmt.Errorf("failed to query denom trace: %w", err)
}
return &traceResp, nil
}
+181
View File
@@ -0,0 +1,181 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
)
// TxResponse represents transaction broadcast response
type TxResponse struct {
TxHash string `json:"txhash"`
Code uint32 `json:"code"`
RawLog string `json:"raw_log"`
GasUsed string `json:"gas_used"`
GasWanted string `json:"gas_wanted"`
Height string `json:"height"`
Events []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
} `json:"events"`
}
// BroadcastTxRequest represents transaction broadcast request
type BroadcastTxRequest struct {
TxBytes []byte `json:"tx_bytes"`
Mode BroadcastMode `json:"mode"`
}
// BroadcastMode represents different broadcast modes
type BroadcastMode string
const (
BroadcastModeSync BroadcastMode = "BROADCAST_MODE_SYNC"
BroadcastModeAsync BroadcastMode = "BROADCAST_MODE_ASYNC"
BroadcastModeBlock BroadcastMode = "BROADCAST_MODE_BLOCK"
)
// BroadcastTx broadcasts a transaction to the network
func (c *StarshipClient) BroadcastTx(ctx context.Context, txBytes []byte, mode BroadcastMode) (*TxResponse, error) {
url := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs", c.baseURL)
reqBody := BroadcastTxRequest{
TxBytes: txBytes,
Mode: mode,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to broadcast transaction: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("broadcast failed with status %d", resp.StatusCode)
}
var broadcastResp struct {
TxResponse TxResponse `json:"tx_response"`
}
if err := json.NewDecoder(resp.Body).Decode(&broadcastResp); err != nil {
return nil, fmt.Errorf("failed to decode broadcast response: %w", err)
}
return &broadcastResp.TxResponse, nil
}
// GetTxResponse represents get transaction response
type GetTxResponse struct {
Tx tx.Tx `json:"tx"`
TxResponse TxResponse `json:"tx_response"`
}
// GetTx queries a transaction by hash
func (c *StarshipClient) GetTx(ctx context.Context, txHash string) (*GetTxResponse, error) {
url := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", c.baseURL, txHash)
var txResp GetTxResponse
if err := c.doRequest(ctx, url, &txResp); err != nil {
return nil, fmt.Errorf("failed to query transaction: %w", err)
}
return &txResp, nil
}
// SimulateRequest represents transaction simulation request
type SimulateRequest struct {
TxBytes []byte `json:"tx_bytes"`
}
// SimulateResponse represents transaction simulation response
type SimulateResponse struct {
GasInfo struct {
GasWanted string `json:"gas_wanted"`
GasUsed string `json:"gas_used"`
} `json:"gas_info"`
Result struct {
Data string `json:"data"`
Log string `json:"log"`
Events []sdk.StringEvent `json:"events"`
} `json:"result"`
}
// SimulateTx simulates a transaction to estimate gas
func (c *StarshipClient) SimulateTx(ctx context.Context, txBytes []byte) (*SimulateResponse, error) {
url := fmt.Sprintf("%s/cosmos/tx/v1beta1/simulate", c.baseURL)
reqBody := SimulateRequest{
TxBytes: txBytes,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal simulate request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create simulate request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to simulate transaction: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("simulation failed with status %d", resp.StatusCode)
}
var simResp SimulateResponse
if err := json.NewDecoder(resp.Body).Decode(&simResp); err != nil {
return nil, fmt.Errorf("failed to decode simulation response: %w", err)
}
return &simResp, nil
}
// WaitForTx waits for a transaction to be included in a block
func (c *StarshipClient) WaitForTx(ctx context.Context, txHash string, timeout time.Duration) (*GetTxResponse, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for transaction %s", txHash)
case <-ticker.C:
tx, err := c.GetTx(ctx, txHash)
if err == nil && tx.TxResponse.Code == 0 {
return tx, nil
}
// Continue waiting if transaction not found or failed
}
}
}
+320
View File
@@ -0,0 +1,320 @@
package client
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
)
// WebSocketClient provides WebSocket client for CometBFT event subscription
type WebSocketClient struct {
baseURL string
conn *websocket.Conn
}
// EventSubscription represents an event subscription
type EventSubscription struct {
Query string
Events chan *SubscriptionEvent
Errors chan error
done chan struct{}
}
// SubscriptionEvent represents an event received via subscription
type SubscriptionEvent struct {
Query string `json:"query"`
Data EventResultData `json:"data"`
Events []any `json:"events,omitempty"`
}
// EventResultData represents the data part of a subscription event
type EventResultData struct {
Type string `json:"type"`
Value any `json:"value"`
}
// JSONRPCRequest represents a JSON-RPC request
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params any `json:"params"`
ID int `json:"id"`
}
// JSONRPCResponse represents a JSON-RPC response
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result any `json:"result,omitempty"`
Error *JSONRPCError `json:"error,omitempty"`
ID int `json:"id"`
}
// JSONRPCError represents a JSON-RPC error
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"`
}
// SubscribeParams represents subscription parameters
type SubscribeParams struct {
Query string `json:"query"`
}
// NewWebSocketClient creates a new WebSocket client
func NewWebSocketClient(baseURL string) *WebSocketClient {
return &WebSocketClient{
baseURL: baseURL,
}
}
// Connect establishes a WebSocket connection to CometBFT
func (ws *WebSocketClient) Connect(ctx context.Context) error {
// Convert HTTP URL to WebSocket URL
wsURL := strings.Replace(ws.baseURL, "http://", "ws://", 1)
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
wsURL += "/websocket"
u, err := url.Parse(wsURL)
if err != nil {
return fmt.Errorf("invalid WebSocket URL: %w", err)
}
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
conn, _, err := dialer.DialContext(ctx, u.String(), nil)
if err != nil {
return fmt.Errorf("failed to connect to WebSocket: %w", err)
}
ws.conn = conn
return nil
}
// Close closes the WebSocket connection
func (ws *WebSocketClient) Close() error {
if ws.conn != nil {
return ws.conn.Close()
}
return nil
}
// Subscribe subscribes to events matching the given query
func (ws *WebSocketClient) Subscribe(ctx context.Context, query string) (*EventSubscription, error) {
if ws.conn == nil {
return nil, fmt.Errorf("WebSocket connection not established")
}
// Send subscription request
req := JSONRPCRequest{
JSONRPC: "2.0",
Method: "subscribe",
Params: SubscribeParams{
Query: query,
},
ID: 1,
}
if err := ws.conn.WriteJSON(req); err != nil {
return nil, fmt.Errorf("failed to send subscription request: %w", err)
}
// Read subscription response
var resp JSONRPCResponse
if err := ws.conn.ReadJSON(&resp); err != nil {
return nil, fmt.Errorf("failed to read subscription response: %w", err)
}
if resp.Error != nil {
return nil, fmt.Errorf("subscription error: %s", resp.Error.Message)
}
// Create subscription
subscription := &EventSubscription{
Query: query,
Events: make(chan *SubscriptionEvent, 100),
Errors: make(chan error, 10),
done: make(chan struct{}),
}
// Start listening for events
go ws.listenForEvents(ctx, subscription)
return subscription, nil
}
// SubscribeToNewBlocks subscribes to new block events
func (ws *WebSocketClient) SubscribeToNewBlocks(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "tm.event = 'NewBlock'")
}
// SubscribeToNewBlockHeaders subscribes to new block header events
func (ws *WebSocketClient) SubscribeToNewBlockHeaders(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "tm.event = 'NewBlockHeader'")
}
// SubscribeToTxEvents subscribes to transaction events
func (ws *WebSocketClient) SubscribeToTxEvents(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "tm.event = 'Tx'")
}
// SubscribeToDIDEvents subscribes to DID module events
func (ws *WebSocketClient) SubscribeToDIDEvents(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "did.v1.EventDIDCreated EXISTS OR did.v1.EventDIDUpdated EXISTS OR did.v1.EventDIDDeactivated EXISTS")
}
// SubscribeToDWNEvents subscribes to DWN module events
func (ws *WebSocketClient) SubscribeToDWNEvents(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "dwn.v1.EventRecordWritten EXISTS OR dwn.v1.EventRecordDeleted EXISTS")
}
// SubscribeToCustomEvents subscribes to custom events with specific attributes
func (ws *WebSocketClient) SubscribeToCustomEvents(ctx context.Context, eventType, attributeKey, attributeValue string) (*EventSubscription, error) {
query := fmt.Sprintf("%s EXISTS", eventType)
if attributeKey != "" && attributeValue != "" {
query += fmt.Sprintf(" AND %s.%s = '%s'", eventType, attributeKey, attributeValue)
}
return ws.Subscribe(ctx, query)
}
// listenForEvents listens for incoming events on the WebSocket connection
func (ws *WebSocketClient) listenForEvents(ctx context.Context, subscription *EventSubscription) {
defer close(subscription.Events)
defer close(subscription.Errors)
for {
select {
case <-ctx.Done():
return
case <-subscription.done:
return
default:
// Set read deadline
if err := ws.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
subscription.Errors <- fmt.Errorf("failed to set read deadline: %w", err)
return
}
var message json.RawMessage
if err := ws.conn.ReadJSON(&message); err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
return
}
subscription.Errors <- fmt.Errorf("failed to read WebSocket message: %w", err)
continue
}
// Try to parse as JSON-RPC response first
var resp JSONRPCResponse
if err := json.Unmarshal(message, &resp); err == nil && resp.Result != nil {
// This is likely an event notification
var event SubscriptionEvent
if eventBytes, err := json.Marshal(resp.Result); err == nil {
if err := json.Unmarshal(eventBytes, &event); err == nil {
event.Query = subscription.Query
select {
case subscription.Events <- &event:
case <-ctx.Done():
return
case <-subscription.done:
return
}
}
}
}
}
}
}
// Unsubscribe unsubscribes from the event subscription
func (ws *WebSocketClient) Unsubscribe(ctx context.Context, subscription *EventSubscription) error {
if ws.conn == nil {
return fmt.Errorf("WebSocket connection not established")
}
// Send unsubscribe request
req := JSONRPCRequest{
JSONRPC: "2.0",
Method: "unsubscribe",
Params: SubscribeParams{
Query: subscription.Query,
},
ID: 2,
}
if err := ws.conn.WriteJSON(req); err != nil {
return fmt.Errorf("failed to send unsubscribe request: %w", err)
}
// Signal the listening goroutine to stop
close(subscription.done)
return nil
}
// Close closes the event subscription
func (sub *EventSubscription) Close() {
if sub.done != nil {
select {
case <-sub.done:
// Already closed
default:
close(sub.done)
}
}
}
// WaitForEvent waits for a specific event with timeout
func (sub *EventSubscription) WaitForEvent(ctx context.Context, timeout time.Duration, eventFilter func(*SubscriptionEvent) bool) (*SubscriptionEvent, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for event")
case err := <-sub.Errors:
return nil, fmt.Errorf("subscription error: %w", err)
case event := <-sub.Events:
if event == nil {
return nil, fmt.Errorf("event channel closed")
}
if eventFilter == nil || eventFilter(event) {
return event, nil
}
}
}
}
// WaitForEventByType waits for an event of a specific type
func (sub *EventSubscription) WaitForEventByType(ctx context.Context, timeout time.Duration, eventType string) (*SubscriptionEvent, error) {
return sub.WaitForEvent(ctx, timeout, func(event *SubscriptionEvent) bool {
// This is a simplified check - in practice, you'd parse the event data more carefully
eventStr := fmt.Sprintf("%v", event.Data.Value)
return strings.Contains(eventStr, eventType)
})
}
// GetAllEvents returns all events received so far (non-blocking)
func (sub *EventSubscription) GetAllEvents() []*SubscriptionEvent {
var events []*SubscriptionEvent
for {
select {
case event := <-sub.Events:
if event == nil {
return events
}
events = append(events, event)
default:
return events
}
}
}
+51
View File
@@ -0,0 +1,51 @@
# E2E Test Configuration for Starship
chain:
id: "sonrtest_1-1"
base_url: "http://localhost:1317"
rpc_url: "http://localhost:26657"
grpc_url: "localhost:9090"
faucet:
url: "http://localhost:8000"
denoms:
staking: "usnr"
normal: "snr"
timeouts:
default: "30s"
tx_wait: "60s"
block_time: "2s"
test_accounts:
acc0:
address: "idx1fcqk3crpnyvyhtd4jepsnx5eat5ehc920epq29"
mnemonic: "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"
acc1:
address: "idx10n78mn09nx0f056wam35wkfvanf37kepuj28x4"
mnemonic: "wealth flavor believe regret funny network recall kiss grape useless pepper cram hint member few certain unveil rather brick bargain curious require crowd raise"
user0:
address: "idx1jyq30438zx0g4urancle25r6tk5td6pgeytpfu"
mnemonic: "love group axis climb enlist evoke cactus sentence mule virtual dose river pepper path chapter ridge merry glow parent swear famous milk two raw"
user1:
address: "idx1wz5qn36kdakkqunkvwuuvpr2l4amd7y0m3qdq6"
mnemonic: "sight buffalo monitor immune awake proof keen help connect steak attack trophy try day know wheel soon gesture switch poverty imitate weird bargain resist"
amounts:
default_fund: "10000000" # 10 SNR
large_fund: "100000000" # 100 SNR
ibc_transfer: "1000000" # 1 SNR
starship:
config_path: "../../chains/standalone.json"
network_name: "devnet"
ports:
rest: 1317
rpc: 26657
grpc: 9090
faucet: 8000
logging:
level: "info"
format: "json"
+423
View File
@@ -0,0 +1,423 @@
module github.com/sonr-io/sonr/test/e2e
go 1.24.1
toolchain go1.24.4
replace (
cosmossdk.io/core => cosmossdk.io/core v0.11.0
cosmossdk.io/store => github.com/evmos/cosmos-sdk/store v0.0.0-20240718141609-414cbd051fbe
github.com/cosmos/cosmos-sdk => github.com/strangelove-ventures/cosmos-sdk v0.0.0-20250317212103-0767f8c5b1e5
github.com/cosmos/evm => github.com/strangelove-ventures/cosmos-evm v0.2.0
github.com/ethereum/go-ethereum => github.com/evmos/go-ethereum v1.10.26-evmos-rc4
github.com/spf13/viper => github.com/spf13/viper v1.17.0
nhooyr.io/websocket => nhooyr.io/websocket v1.8.17
)
replace (
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.8.1
github.com/sonr-io/sonr => ../../
github.com/sonr-io/sonr/crypto => ../../crypto
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
)
require (
cosmossdk.io/math v1.5.3
github.com/cosmos/cosmos-sdk v0.53.4
github.com/cosmos/ibc-go/v8 v8.7.0
github.com/stretchr/testify v1.10.0
)
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/auth v0.6.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.1.9 // indirect
cloud.google.com/go/storage v1.41.0 // indirect
cosmossdk.io/api v0.7.6 // indirect
cosmossdk.io/client/v2 v2.0.0-beta.7 // indirect
cosmossdk.io/collections v0.4.0 // indirect
cosmossdk.io/core v0.12.0 // indirect
cosmossdk.io/depinject v1.1.0 // indirect
cosmossdk.io/errors v1.0.1 // indirect
cosmossdk.io/log v1.6.1 // indirect
cosmossdk.io/orm v1.0.0-beta.3 // indirect
cosmossdk.io/store v1.1.1 // indirect
cosmossdk.io/x/circuit v0.1.1 // indirect
cosmossdk.io/x/evidence v0.1.1 // indirect
cosmossdk.io/x/feegrant v0.1.1 // indirect
cosmossdk.io/x/nft v0.1.0 // indirect
cosmossdk.io/x/tx v0.13.7 // indirect
cosmossdk.io/x/upgrade v0.1.4 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.1 // indirect
github.com/CosmWasm/wasmvm v1.5.8 // indirect
github.com/DataDog/datadog-go v3.2.0+incompatible // indirect
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect
github.com/Oudwins/zog v0.21.6 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/VictoriaMetrics/fastcache v1.6.0 // indirect
github.com/Workiva/go-datastructures v1.1.3 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/asynkron/protoactor-go v0.0.0-20240822202345-3c0e61ca19c9 // indirect
github.com/aws/aws-sdk-go v1.55.6 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
github.com/biter777/countries v1.7.5 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/bwesterb/go-ristretto v1.2.3 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/caddyserver/certmagic v0.21.6 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cockroachdb/apd/v2 v2.0.2 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/cockroachdb/crlib v0.0.0-20241015224233-894974b3ad94 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/pebble v1.1.2 // indirect
github.com/cockroachdb/pebble/v2 v2.0.3 // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/swiss v0.0.0-20250624142022-d6e517c1d961 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/cometbft/cometbft v0.38.17 // indirect
github.com/cometbft/cometbft-db v0.14.1 // indirect
github.com/consensys/bavard v0.1.27 // indirect
github.com/consensys/gnark-crypto v0.16.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-db v1.1.1 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
github.com/cosmos/evm v0.1.0 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/gogoproto v1.7.0 // indirect
github.com/cosmos/iavl v1.2.2 // indirect
github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8 v8.1.1 // indirect
github.com/cosmos/ibc-apps/modules/rate-limiting/v8 v8.0.0 // indirect
github.com/cosmos/ibc-go/modules/capability v1.0.1 // indirect
github.com/cosmos/ibc-go/modules/light-clients/08-wasm v0.1.1-0.20231213092650-57fcdb9a9a9d // indirect
github.com/cosmos/ics23/go v0.11.0 // indirect
github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect
github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 // indirect
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emicklei/dot v1.6.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/ethereum/go-ethereum v1.16.1 // indirect
github.com/extism/go-sdk v1.7.1 // indirect
github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/flynn/noise v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gammazero/deque v1.0.0 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gogo/status v1.1.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/orderedcode v0.0.1 // indirect
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-getter v1.7.9 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.3 // indirect
github.com/hashicorp/go-plugin v1.5.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/hibiken/asynq v0.25.1 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/huandu/skiplist v1.2.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/iancoleman/orderedmap v0.3.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/improbable-eng/grpc-web v0.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/boxo v0.32.0 // indirect
github.com/ipfs/go-bitfield v1.1.0 // indirect
github.com/ipfs/go-block-format v0.2.1 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/ipfs/go-datastore v0.8.2 // indirect
github.com/ipfs/go-ds-measure v0.2.2 // indirect
github.com/ipfs/go-fs-lock v0.1.1 // indirect
github.com/ipfs/go-ipfs-cmds v0.14.1 // indirect
github.com/ipfs/go-ipld-cbor v0.2.0 // indirect
github.com/ipfs/go-ipld-format v0.6.1 // indirect
github.com/ipfs/go-ipld-legacy v0.2.1 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.6.0 // indirect
github.com/ipfs/go-metrics-interface v0.3.0 // indirect
github.com/ipfs/go-unixfsnode v1.10.1 // indirect
github.com/ipfs/kubo v0.35.0 // indirect
github.com/ipld/go-car/v2 v2.14.3 // indirect
github.com/ipld/go-codec-dagpb v1.7.0 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/ipshipyard/p2p-forge v0.5.1 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/koron/go-ssdp v0.0.5 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/echo-jwt/v4 v4.3.1 // indirect
github.com/labstack/echo/v4 v4.13.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
github.com/libp2p/go-libp2p v0.41.1 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.33.1 // indirect
github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect
github.com/libp2p/go-libp2p-record v0.3.1 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-netroute v0.2.2 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/linxGnu/grocksdb v1.9.8 // indirect
github.com/lithammer/shortuuid/v4 v4.0.0 // indirect
github.com/lmittmann/tint v1.0.3 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.0.0 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect
github.com/minio/highwayhash v1.0.3 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.16.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.1 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.6.1 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/orcaman/concurrent-map v1.0.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/ice/v4 v4.0.8 // indirect
github.com/pion/interceptor v0.1.39 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.18 // indirect
github.com/pion/sctp v1.8.37 // indirect
github.com/pion/sdp/v3 v3.0.10 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v4 v4.0.10 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/tsdb v0.10.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.50.1 // indirect
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/redis/go-redis/v9 v9.11.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/samber/lo v1.47.0 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/sonr-io/sonr v0.0.0-00010101000000-000000000000 // indirect
github.com/sonr-io/sonr/crypto v0.0.0-00010101000000-000000000000 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/strangelove-ventures/tokenfactory v0.50.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect
github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect
github.com/whyrusleeping/cbor-gen v0.1.2 // indirect
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zondax/hid v0.9.2 // indirect
github.com/zondax/ledger-go v0.14.3 // indirect
go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/dig v1.18.0 // indirect
go.uber.org/fx v1.23.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.35.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/api v0.186.0 // indirect
google.golang.org/genproto v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
nhooyr.io/websocket v1.8.10 // indirect
pgregory.net/rapid v1.1.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect
)
+3376
View File
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
# WebAuthn Integration Tests
## Overview
This test suite provides comprehensive end-to-end testing for the WebAuthn implementation in the Sonr blockchain's DID module. The tests validate the complete WebAuthn registration, authentication, and key management workflows.
## Test Coverage
The test suite covers the following scenarios:
1. **Attestation Parsing**
- Validates parsing of CBOR attestation objects
- Checks public key extraction
- Verifies algorithm and authenticator data detection
2. **Registration Flow**
- Complete WebAuthn credential registration
- Challenge verification
- Origin validation
- DID document creation with WebAuthn credentials
- Credential uniqueness enforcement
3. **Signature Verification**
- WebAuthn assertion verification
- User presence and verification flag checking
- Multi-algorithm signature support (ES256, RS256, EdDSA)
- Counter increment validation
4. **Security Scenarios**
- Challenge replay attack prevention
- Invalid origin rejection
- Oversized credential handling
- Credential ID reuse prevention
## Test Methodology
- Uses Cosmos SDK testing framework
- Employs table-driven tests for multiple scenarios
- Mocks cryptographic keys and challenge responses
- Validates both positive and negative test cases
## Running Tests
```bash
go test github.com/sonr-io/sonr/test/e2e/tests -v
```
## Dependencies
- Cosmos SDK v0.50.14
- Internal WebAuthn libraries
- testify assertion library
+136
View File
@@ -0,0 +1,136 @@
package basic
import (
"context"
"testing"
"cosmossdk.io/math"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/test/e2e/utils"
)
func TestBasicChain(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("node_info", func(t *testing.T) {
utils.AssertNodeInfo(t, cfg, cfg.ChainID)
})
t.Run("validate_pre_funded_accounts", func(t *testing.T) {
// Check pre-funded accounts from localnet
acc0Addr := "idx1fcqk3crpnyvyhtd4jepsnx5eat5ehc920epq29"
acc1Addr := "idx10n78mn09nx0f056wam35wkfvanf37kepuj28x4"
// Verify acc0 has balance
balance0, err := cfg.Client.GetBalance(ctx, acc0Addr, cfg.StakingDenom)
require.NoError(t, err, "failed to query acc0 balance")
require.True(t, balance0.GT(math.ZeroInt()), "acc0 should have balance")
// Verify acc1 has balance
balance1, err := cfg.Client.GetBalance(ctx, acc1Addr, cfg.StakingDenom)
require.NoError(t, err, "failed to query acc1 balance")
require.True(t, balance1.GT(math.ZeroInt()), "acc1 should have balance")
})
t.Run("bank_params", func(t *testing.T) {
bankParams, err := cfg.Client.GetBankParams(ctx)
require.NoError(t, err, "failed to query bank params")
require.NotNil(t, bankParams, "bank params should not be nil")
})
t.Run("supply_queries", func(t *testing.T) {
// Query total supply of test denom
testSupply, err := cfg.Client.GetSupply(ctx, "test")
require.NoError(t, err, "failed to query test supply")
require.True(t, testSupply.GT(math.ZeroInt()), "test supply should be greater than zero")
// Query total supply of staking denom
stakingSupply, err := cfg.Client.GetSupply(ctx, cfg.StakingDenom)
require.NoError(t, err, "failed to query staking supply")
require.True(t, stakingSupply.GT(math.ZeroInt()), "staking supply should be greater than zero")
})
t.Run("balance_operations", func(t *testing.T) {
// Use pre-funded account from localnet
testAddr := "idx1fcqk3crpnyvyhtd4jepsnx5eat5ehc920epq29"
// Get all balances
balances, err := cfg.Client.GetAllBalances(ctx, testAddr)
require.NoError(t, err, "failed to query all balances")
require.NotEmpty(t, balances, "user should have at least one balance")
// Check specific balance for staking denom
balance, err := cfg.Client.GetBalance(ctx, testAddr, cfg.StakingDenom)
require.NoError(t, err, "failed to query specific balance")
require.True(t, balance.GT(math.ZeroInt()), "balance should be greater than zero")
})
}
func TestFaucetOperations(t *testing.T) {
t.Skip("Skipping faucet tests - localnet doesn't have a faucet")
cfg := utils.NewTestConfig()
ctx := context.Background()
tests := []struct {
name string
fundAmount math.Int
expectError bool
}{
{
name: "normal_funding",
fundAmount: math.NewInt(1_000_000),
expectError: false,
},
{
name: "large_funding",
fundAmount: math.NewInt(100_000_000),
expectError: false,
},
{
name: "zero_funding",
fundAmount: math.ZeroInt(),
expectError: false, // Faucet should handle this gracefully
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
users := utils.GetDefaultTestUsers(tt.fundAmount, cfg.NormalDenom)
testUser := users[0]
err := cfg.FaucetClient.FundTestUsers(ctx, []utils.CreateTestUser{testUser})
if tt.expectError {
require.Error(t, err, "expected funding to fail")
} else {
require.NoError(t, err, "funding should succeed")
// Wait for transaction to be included
err = utils.WaitForBlocks(ctx, cfg, 2)
require.NoError(t, err, "failed to wait for blocks")
// Verify balance if funding was expected to succeed
if !tt.fundAmount.IsZero() {
utils.AssertBalance(t, cfg, testUser.Address, testUser.Denom, testUser.Amount)
}
}
})
}
}
func TestChainConnectivity(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("rest_api_connectivity", func(t *testing.T) {
// Test REST API connectivity by querying node info
nodeInfo, err := cfg.Client.GetNodeInfo(ctx)
require.NoError(t, err, "REST API should be accessible")
require.Equal(t, cfg.ChainID, nodeInfo.DefaultNodeInfo.Network, "chain ID should match")
})
t.Run("faucet_connectivity", func(t *testing.T) {
t.Skip("Skipping faucet connectivity test - localnet doesn't have a faucet")
})
}
+299
View File
@@ -0,0 +1,299 @@
package dex
import (
"context"
"testing"
"time"
"cosmossdk.io/math"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/test/e2e/utils"
dextypes "github.com/sonr-io/sonr/x/dex/types"
)
// TestDEXModuleOperations tests the DEX module E2E operations
func TestDEXModuleOperations(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("query_dex_params", func(t *testing.T) {
// Query DEX module parameters
resp, err := cfg.Client.QueryDEXParams(ctx)
require.NoError(t, err, "failed to query DEX params")
require.NotNil(t, resp, "DEX params should not be nil")
require.True(t, resp.Params.Enabled, "DEX module should be enabled")
})
t.Run("register_dex_account", func(t *testing.T) {
// Register a DEX account for testing
did := "did:sonr:e2e_test_user"
connectionID := "connection-0"
features := []string{"swap", "liquidity"}
msg := &dextypes.MsgRegisterDEXAccount{
Did: did,
ConnectionId: connectionID,
Features: features,
}
// Sign and broadcast transaction
txResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, msg)
require.NoError(t, err, "failed to register DEX account")
require.Equal(t, uint32(0), txResp.Code, "transaction should succeed")
// Query the created account
queryResp, err := cfg.Client.QueryDEXAccount(ctx, did, connectionID)
require.NoError(t, err, "failed to query DEX account")
require.NotNil(t, queryResp, "DEX account should exist")
require.Equal(t, did, queryResp.Account.Did)
require.Equal(t, connectionID, queryResp.Account.ConnectionId)
})
t.Run("execute_swap", func(t *testing.T) {
// Setup: Register account first
did := "did:sonr:e2e_swap_user"
connectionID := "connection-0"
registerMsg := &dextypes.MsgRegisterDEXAccount{
Did: did,
ConnectionId: connectionID,
Features: []string{"swap"},
}
txResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, registerMsg)
require.NoError(t, err, "failed to register DEX account for swap")
require.Equal(t, uint32(0), txResp.Code, "registration should succeed")
// Execute swap
swapMsg := &dextypes.MsgExecuteSwap{
Did: did,
ConnectionId: connectionID,
SourceDenom: cfg.StakingDenom,
TargetDenom: "uosmo",
Amount: math.NewInt(1000),
MinAmountOut: math.NewInt(900),
Route: "pool:1",
}
txResp, err = cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, swapMsg)
require.NoError(t, err, "failed to execute swap")
require.Equal(t, uint32(0), txResp.Code, "swap should succeed")
})
t.Run("provide_liquidity", func(t *testing.T) {
// Setup: Register account with liquidity feature
did := "did:sonr:e2e_lp_user"
connectionID := "connection-0"
registerMsg := &dextypes.MsgRegisterDEXAccount{
Did: did,
ConnectionId: connectionID,
Features: []string{"liquidity"},
}
txResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, registerMsg)
require.NoError(t, err, "failed to register DEX account for liquidity")
require.Equal(t, uint32(0), txResp.Code, "registration should succeed")
// Provide liquidity
liquidityMsg := &dextypes.MsgProvideLiquidity{
Did: did,
ConnectionId: connectionID,
PoolId: "1",
Assets: sdk.NewCoins(
sdk.NewCoin(cfg.StakingDenom, math.NewInt(1000)),
sdk.NewCoin("uosmo", math.NewInt(1000)),
),
MinShares: math.NewInt(100),
Timeout: time.Now().Add(5 * time.Minute),
}
txResp, err = cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, liquidityMsg)
require.NoError(t, err, "failed to provide liquidity")
require.Equal(t, uint32(0), txResp.Code, "liquidity provision should succeed")
})
t.Run("create_limit_order", func(t *testing.T) {
// Setup: Register account with order feature
did := "did:sonr:e2e_order_user"
connectionID := "connection-0"
registerMsg := &dextypes.MsgRegisterDEXAccount{
Did: did,
ConnectionId: connectionID,
Features: []string{"order"},
}
txResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, registerMsg)
require.NoError(t, err, "failed to register DEX account for orders")
require.Equal(t, uint32(0), txResp.Code, "registration should succeed")
// Create limit order
orderMsg := &dextypes.MsgCreateLimitOrder{
Did: did,
ConnectionId: connectionID,
SellDenom: cfg.StakingDenom,
BuyDenom: "uosmo",
Amount: math.NewInt(1000),
Price: math.LegacyNewDec(1),
Expiration: time.Now().Add(24 * time.Hour),
}
txResp, err = cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, orderMsg)
require.NoError(t, err, "failed to create limit order")
require.Equal(t, uint32(0), txResp.Code, "order creation should succeed")
// TODO: Query and verify the order was created
})
t.Run("query_dex_accounts", func(t *testing.T) {
// Query all DEX accounts
resp, err := cfg.Client.QueryAllDEXAccounts(ctx)
require.NoError(t, err, "failed to query all DEX accounts")
require.NotNil(t, resp, "response should not be nil")
// Should have at least the accounts created in previous tests
require.GreaterOrEqual(t, len(resp.Accounts), 1, "should have at least one account")
})
t.Run("query_dex_history", func(t *testing.T) {
// Query transaction history for a DID
did := "did:sonr:e2e_swap_user"
resp, err := cfg.Client.QueryDEXHistory(ctx, did)
require.NoError(t, err, "failed to query DEX history")
require.NotNil(t, resp, "response should not be nil")
// Should have at least one transaction from the swap test
require.GreaterOrEqual(t, len(resp.History), 0, "history may be empty if ICA is not fully setup")
})
t.Run("cancel_order", func(t *testing.T) {
// Setup: Register account and create an order first
did := "did:sonr:e2e_cancel_user"
connectionID := "connection-0"
// Register account
registerMsg := &dextypes.MsgRegisterDEXAccount{
Did: did,
ConnectionId: connectionID,
Features: []string{"order"},
}
txResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, registerMsg)
require.NoError(t, err, "failed to register DEX account")
require.Equal(t, uint32(0), txResp.Code, "registration should succeed")
// Create an order
orderMsg := &dextypes.MsgCreateLimitOrder{
Did: did,
ConnectionId: connectionID,
SellDenom: cfg.StakingDenom,
BuyDenom: "uosmo",
Amount: math.NewInt(500),
Price: math.LegacyNewDec(1),
Expiration: time.Now().Add(24 * time.Hour),
}
createResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, orderMsg)
require.NoError(t, err, "failed to create order")
require.Equal(t, uint32(0), createResp.Code, "order creation should succeed")
// Extract order ID from events (mock for now)
orderID := "order-1" // In real test, extract from createResp.Events
// Cancel the order
cancelMsg := &dextypes.MsgCancelOrder{
Did: did,
ConnectionId: connectionID,
OrderId: orderID,
}
cancelResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, cancelMsg)
require.NoError(t, err, "failed to cancel order")
require.Equal(t, uint32(0), cancelResp.Code, "order cancellation should succeed")
})
t.Run("remove_liquidity", func(t *testing.T) {
// Setup: Register account and provide liquidity first
did := "did:sonr:e2e_remove_lp_user"
connectionID := "connection-0"
// Register account
registerMsg := &dextypes.MsgRegisterDEXAccount{
Did: did,
ConnectionId: connectionID,
Features: []string{"liquidity"},
}
txResp, err := cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, registerMsg)
require.NoError(t, err, "failed to register DEX account")
require.Equal(t, uint32(0), txResp.Code, "registration should succeed")
// First provide liquidity
provideMsg := &dextypes.MsgProvideLiquidity{
Did: did,
ConnectionId: connectionID,
PoolId: "1",
Assets: sdk.NewCoins(
sdk.NewCoin(cfg.StakingDenom, math.NewInt(2000)),
sdk.NewCoin("uosmo", math.NewInt(2000)),
),
MinShares: math.NewInt(200),
Timeout: time.Now().Add(5 * time.Minute),
}
txResp, err = cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, provideMsg)
require.NoError(t, err, "failed to provide liquidity")
require.Equal(t, uint32(0), txResp.Code, "liquidity provision should succeed")
// Remove liquidity
removeMsg := &dextypes.MsgRemoveLiquidity{
Did: did,
ConnectionId: connectionID,
PoolId: "1",
Shares: math.NewInt(100),
MinAmounts: sdk.NewCoins(
sdk.NewCoin(cfg.StakingDenom, math.NewInt(900)),
sdk.NewCoin("uosmo", math.NewInt(900)),
),
Timeout: time.Now().Add(5 * time.Minute),
}
txResp, err = cfg.Client.SignAndBroadcastTx(ctx, cfg.TestAccount, removeMsg)
require.NoError(t, err, "failed to remove liquidity")
require.Equal(t, uint32(0), txResp.Code, "liquidity removal should succeed")
})
}
// TestDEXIBCIntegration tests IBC-related DEX operations
func TestDEXIBCIntegration(t *testing.T) {
t.Skip("Skipping IBC integration tests - requires full IBC setup")
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("cross_chain_swap", func(t *testing.T) {
// This test would require an actual IBC connection to another chain
// For now, we skip it but document the expected behavior
// 1. Register ICA account on remote chain
// 2. Fund the ICA account
// 3. Execute swap on remote chain
// 4. Verify swap execution through events/callbacks
_ = cfg
_ = ctx
})
t.Run("multi_chain_accounts", func(t *testing.T) {
// Test managing accounts across multiple chains
// This would require multiple IBC connections
// 1. Register accounts on Osmosis, Cosmos Hub, etc.
// 2. Query all accounts for a single DID
// 3. Verify each account has different connection IDs
_ = cfg
_ = ctx
})
}
+165
View File
@@ -0,0 +1,165 @@
package ibc
import (
"context"
"testing"
"cosmossdk.io/math"
transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/test/e2e/utils"
)
func TestIBCBasic(t *testing.T) {
t.Skip("Skipping IBC tests - localnet doesn't have IBC channels")
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("ibc_channel_exists", func(t *testing.T) {
// Verify that IBC transfer channels exist
channelID := utils.AssertTransferChannelExists(t, cfg)
require.NotEmpty(t, channelID, "transfer channel should exist")
})
t.Run("ibc_channels_query", func(t *testing.T) {
// Query all IBC channels
channels, err := cfg.Client.GetChannels(ctx)
require.NoError(t, err, "failed to query IBC channels")
require.NotEmpty(t, channels.Channels, "should have at least one IBC channel")
// Verify we have transfer channels
hasTransferChannel := false
for _, channel := range channels.Channels {
if channel.PortID == "transfer" {
hasTransferChannel = true
require.Equal(t, "STATE_OPEN", channel.State, "transfer channel should be open")
break
}
}
require.True(t, hasTransferChannel, "should have at least one transfer channel")
})
t.Run("ibc_channel_details", func(t *testing.T) {
// Get transfer channel ID
channelID, err := cfg.Client.GetTransferChannel(ctx)
require.NoError(t, err, "failed to get transfer channel")
// Query specific channel details
channel, err := cfg.Client.GetChannel(ctx, "transfer", channelID)
require.NoError(t, err, "failed to query channel details")
require.NotNil(t, channel, "channel response should not be nil")
require.Equal(t, "STATE_OPEN", channel.Channel.State.String(), "channel should be open")
})
}
func TestIBCDenomTrace(t *testing.T) {
t.Skip("Skipping IBC tests - localnet doesn't have IBC channels")
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("denom_trace_generation", func(t *testing.T) {
// Get transfer channel for testing
channelID, err := cfg.Client.GetTransferChannel(ctx)
require.NoError(t, err, "failed to get transfer channel")
// Generate IBC denom trace for testing
denomTrace := transfertypes.ParseDenomTrace(
transfertypes.GetPrefixedDenom("transfer", channelID, cfg.NormalDenom),
)
ibcDenom := denomTrace.IBCDenom()
require.NotEmpty(t, ibcDenom, "IBC denom should not be empty")
require.Contains(t, ibcDenom, "ibc/", "IBC denom should have ibc/ prefix")
})
}
// TestIBCTransferSimulation tests IBC transfer logic without actual multi-chain setup
func TestIBCTransferSimulation(t *testing.T) {
t.Skip("Skipping IBC tests - localnet doesn't have IBC channels")
cfg := utils.NewTestConfig()
ctx := context.Background()
tests := []struct {
name string
transferAmount math.Int
expectError bool
}{
{
name: "normal_transfer_amount",
transferAmount: math.NewInt(1_000_000), // 1 SNR
expectError: false,
},
{
name: "large_transfer_amount",
transferAmount: math.NewInt(50_000_000), // 50 SNR
expectError: false,
},
{
name: "zero_transfer_amount",
transferAmount: math.ZeroInt(),
expectError: true, // Should fail validation
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup test user with sufficient funds
fundAmount := math.NewInt(100_000_000) // 100 SNR
users := utils.SetupTestUsers(t, cfg, fundAmount)
sourceUser := users[0]
// Verify user has sufficient balance before transfer
if !tt.expectError && tt.transferAmount.GT(math.ZeroInt()) {
utils.AssertBalanceGreaterThan(t, cfg, sourceUser.Address, sourceUser.Denom, tt.transferAmount)
}
// Get transfer channel
channelID, err := cfg.Client.GetTransferChannel(ctx)
require.NoError(t, err, "failed to get transfer channel")
// Create IBC denom for destination
denomTrace := transfertypes.ParseDenomTrace(
transfertypes.GetPrefixedDenom("transfer", channelID, cfg.NormalDenom),
)
ibcDenom := denomTrace.IBCDenom()
// Validate transfer parameters
if tt.expectError {
require.True(t, tt.transferAmount.IsZero() || tt.transferAmount.IsNegative(),
"invalid transfer amounts should be caught")
} else {
require.True(t, tt.transferAmount.GT(math.ZeroInt()),
"valid transfer amounts should be positive")
require.NotEmpty(t, ibcDenom, "IBC denom should be generated")
}
})
}
}
func TestIBCConnectionStatus(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("connection_existence", func(t *testing.T) {
// Query channels to find connection information
channels, err := cfg.Client.GetChannels(ctx)
require.NoError(t, err, "failed to query channels")
if len(channels.Channels) > 0 {
// Test connection details for first channel
channel := channels.Channels[0]
require.NotEmpty(t, channel.ConnectionHops, "channel should have connection hops")
if len(channel.ConnectionHops) > 0 {
connectionID := channel.ConnectionHops[0]
// Query connection details
connection, err := cfg.Client.GetConnection(ctx, connectionID)
require.NoError(t, err, "failed to query connection")
require.NotNil(t, connection, "connection should exist")
require.Equal(t, "STATE_OPEN", connection.Connection.State, "connection should be open")
}
}
})
}
+295
View File
@@ -0,0 +1,295 @@
# Event Emission E2E Tests
This directory contains comprehensive End-to-End (E2E) tests for event emissions across Sonr blockchain modules, specifically focusing on the newly implemented typed Protobuf events for the DID and DWN modules.
## Test Coverage
### DID Module Events
- `EventDIDCreated` - Emitted when a new DID is created
- `EventDIDUpdated` - Emitted when a DID is updated
- `EventDIDDeactivated` - Emitted when a DID is deactivated
- `EventVerificationMethodAdded` - Emitted when a verification method is added
- `EventVerificationMethodRemoved` - Emitted when a verification method is removed ⭐
- `EventServiceAdded` - Emitted when a service is added to a DID ⭐
- `EventServiceRemoved` - Emitted when a service is removed from a DID ⭐
- `EventWebAuthnRegistered` - Emitted when a WebAuthn credential is registered ⭐
- `EventExternalWalletLinked` - Emitted when an external wallet is linked ⭐
### DWN Module Events
- `EventRecordWritten` - Emitted when a record is written to DWN
- `EventRecordDeleted` - Emitted when a record is deleted from DWN
- `EventProtocolConfigured` - Emitted when a protocol is configured ⭐
- `EventPermissionGranted` - Emitted when a permission is granted ⭐
- `EventPermissionRevoked` - Emitted when a permission is revoked ⭐
- `EventVaultCreated` - Emitted when a vault is created ⭐
- `EventVaultKeysRotated` - Emitted when vault keys are rotated ⭐
⭐ = Newly implemented events being tested
## Test Structure
### `events_test.go`
The main test file contains the following test suites:
#### `EventEmissionTestSuite`
Main test suite that validates:
1. **Real Transaction Event Emissions** (`TestDIDModuleEventEmissions`, `TestDWNModuleEventEmissions`)
- Executes actual transactions that trigger events
- Verifies events are emitted correctly with proper attributes
- Tests each event type individually
2. **Event Persistence and Replay** (`TestEventPersistenceAndReplay`)
- Verifies events persist across multiple queries
- Tests event queryability by attributes
- Ensures event data consistency over time
3. **Event Querying** (`TestEventQuerying`)
- Tests CometBFT query syntax patterns
- Validates filtering by event type, creator, and custom attributes
- Tests complex query conditions
4. **Multi-Event Transactions** (`TestMultiEventTransactions`)
- Tests transactions that emit multiple events
- Verifies correct event ordering
- Validates block height consistency across events
5. **Event Subscription** (`TestEventSubscription`)
- Tests WebSocket-based event subscription via CometBFT
- Subscribes to new blocks, transactions, and custom events
- Validates real-time event streaming
6. **Event Attribute Validation** (`TestEventAttributeValidation`)
- Verifies all required attributes are present
- Validates attribute values are correctly populated
- Tests attribute consistency
## Client Extensions
### Enhanced StarshipClient (`client/chain.go`)
Extended the existing StarshipClient with comprehensive event querying capabilities:
- `QueryEventsByHeight(height)` - Query events by block height
- `QueryEventsByType(eventType, minHeight, maxHeight)` - Query by event type
- `QueryEventsByAttribute(key, value, minHeight, maxHeight)` - Query by attribute
- `SearchEvents(query, minHeight, maxHeight)` - General CometBFT query search
- `GetLatestBlockHeight()` - Get current block height
- `WaitForNextBlock()` - Wait for next block production
- `FilterEventsByType(events, eventType)` - Filter events by type
- `GetEventAttribute(event, key)` - Extract specific attribute values
### WebSocket Client (`client/websocket.go`)
New WebSocket client for real-time event subscription:
- `Connect()` - Establish WebSocket connection to CometBFT
- `Subscribe(query)` - Subscribe to events matching query
- `SubscribeToNewBlocks()` - Subscribe to new block events
- `SubscribeToTxEvents()` - Subscribe to transaction events
- `SubscribeToDIDEvents()` - Subscribe to DID module events
- `SubscribeToDWNEvents()` - Subscribe to DWN module events
- `WaitForEvent(timeout, filter)` - Wait for specific events
- `WaitForEventByType(timeout, eventType)` - Wait for events by type
- `Unsubscribe()` - Unsubscribe from events
## Running the Tests
### Prerequisites
1. **Start the Sonr testnet:**
```bash
make testnet # or make start
```
2. **Ensure IPFS is running** (required for DWN tests):
```bash
make ipfs-up
```
3. **Verify chain is running:**
```bash
curl http://localhost:1317/cosmos/base/tendermint/v1beta1/node_info
```
### Run Event Tests
```bash
# Run all event tests
cd test/e2e
go test -v ./tests/modules/ -run TestEventEmission
# Run specific test suites
go test -v ./tests/modules/ -run TestEventEmissionTestSuite/TestDIDModuleEventEmissions
go test -v ./tests/modules/ -run TestEventEmissionTestSuite/TestEventSubscription
go test -v ./tests/modules/ -run TestEventEmissionTestSuite/TestEventPersistenceAndReplay
# Run with detailed logging
go test -v ./tests/modules/ -run TestEventEmission -args -test.v
```
### Run Integration Tests (for comparison)
```bash
# Run the existing integration tests
cd ../..
go test -v ./test/ -run TestEventIntegration
```
## Configuration
### Default Test Configuration (`utils/utils.go`)
- **Chain ID**: `sonrtest_1-1`
- **Base URL**: `http://localhost:1317` (REST API)
- **WebSocket URL**: `ws://localhost:26657/websocket` (CometBFT WebSocket)
- **Staking Denom**: `usnr`
- **Normal Denom**: `snr`
### Pre-funded Test Accounts
The tests use pre-funded localnet accounts:
- `idx1fcqk3crpnyvyhtd4jepsnx5eat5ehc920epq29` (Account 0)
- `idx10n78mn09nx0f056wam35wkfvanf37kepuj28x4` (Account 1)
- `idx1xygwjmmj8rq3rq3k4adqvhd55x5yqjc8ktcm7e` (Account 2)
## Implementation Status
### ✅ Completed Features
1. **Event Querying Infrastructure**
- REST API event queries
- Block height filtering
- Attribute-based filtering
- CometBFT query syntax support
2. **WebSocket Event Subscription**
- Real-time event streaming
- Custom query subscriptions
- Event filtering and waiting
3. **Test Framework**
- Comprehensive test structure
- Mock transaction creation
- Event validation helpers
- Multi-event testing
### 🚧 In Progress / TODO
1. **Real Transaction Building**
- Currently using mock transactions for testing
- Need to implement actual DID/DWN message building and signing
- Integration with existing transaction building utilities
2. **Complete Event Coverage**
- Some event tests are marked as "Skip" pending real transaction implementation
- Need to create actual transactions for each event type
3. **Chain Restart Testing**
- Event persistence across chain restarts
- Historical event replay validation
4. **Performance Testing**
- Event query performance under load
- WebSocket subscription scalability
- Large event volume handling
## Key Testing Patterns
### Event Validation Pattern
```go
// 1. Execute transaction
txResp := suite.createTestTransaction(...)
// 2. Wait for inclusion
finalTx, err := suite.cfg.Client.WaitForTx(ctx, txResp.TxHash, 30*time.Second)
// 3. Filter and validate events
events := client.FilterEventsByType(finalTx.TxResponse.Events, "EventType")
require.NotEmpty(t, events, "should emit EventType")
// 4. Validate attributes
event := events[0]
value, found := client.GetEventAttribute(event, "key")
require.True(t, found, "attribute should be present")
require.Equal(t, expectedValue, value, "attribute value should match")
```
### WebSocket Subscription Pattern
```go
// 1. Connect to WebSocket
wsClient := client.NewWebSocketClient("ws://localhost:26657")
err := wsClient.Connect(ctx)
// 2. Subscribe to events
subscription, err := wsClient.Subscribe(ctx, "custom.query='value'")
// 3. Trigger event (execute transaction)
txResp := suite.executeTransaction(...)
// 4. Wait for event
event, err := subscription.WaitForEvent(ctx, 30*time.Second, filterFunc)
```
### Query Testing Pattern
```go
// 1. Record start height
startHeight, err := suite.cfg.Client.GetLatestBlockHeight(ctx)
// 2. Execute transactions
// ... create multiple transactions
// 3. Query events with filters
events, err := suite.cfg.Client.QueryEventsByType(ctx, "EventType", startHeight, 0)
// 4. Validate results
require.GreaterOrEqual(t, len(events.Events), expectedCount)
```
## Troubleshooting
### Common Issues
1. **WebSocket Connection Failed**
- Ensure CometBFT is running on port 26657
- Check WebSocket endpoint configuration
- Verify network connectivity
2. **Event Not Found**
- Verify transaction was actually executed
- Check event type spelling and case sensitivity
- Confirm transaction succeeded (code = 0)
3. **Query Timeout**
- Increase timeout values for slow networks
- Check block production is active
- Verify query syntax is correct
4. **Missing Events**
- Ensure event emission is implemented in keeper
- Verify protobuf event definitions match
- Check transaction actually triggered the event
### Debug Commands
```bash
# Check chain status
curl http://localhost:1317/cosmos/base/tendermint/v1beta1/node_info
# Query latest block
curl http://localhost:1317/cosmos/base/tendermint/v1beta1/blocks/latest
# Check WebSocket endpoint
curl -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: test" -H "Sec-WebSocket-Version: 13" http://localhost:26657/websocket
# Query specific transaction
curl http://localhost:1317/cosmos/tx/v1beta1/txs/{TX_HASH}
```
## Future Enhancements
1. **Event Analytics Dashboard** - Real-time event monitoring and analytics
2. **Event Replay Service** - Historical event streaming service
3. **Event Benchmarking** - Performance testing for high event volumes
4. **Cross-Chain Event Testing** - IBC event emission testing
5. **Event Schema Validation** - Automatic protobuf schema compliance testing
+615
View File
@@ -0,0 +1,615 @@
package modules
import (
"context"
"fmt"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/sonr-io/sonr/test/e2e/client"
"github.com/sonr-io/sonr/test/e2e/utils"
)
// EventEmissionTestSuite tests comprehensive event emissions across modules
type EventEmissionTestSuite struct {
suite.Suite
cfg *utils.TestConfig
ctx context.Context
cancel context.CancelFunc
// Test user addresses - using pre-funded localnet accounts
userAddrs []string
}
func TestEventEmissionTestSuite(t *testing.T) {
suite.Run(t, new(EventEmissionTestSuite))
}
func (suite *EventEmissionTestSuite) SetupSuite() {
suite.cfg = utils.NewTestConfig()
suite.ctx, suite.cancel = context.WithTimeout(context.Background(), 10*time.Minute)
// Use pre-funded accounts from localnet
suite.userAddrs = []string{
"idx1fcqk3crpnyvyhtd4jepsnx5eat5ehc920epq29", // Pre-funded account 0
"idx10n78mn09nx0f056wam35wkfvanf37kepuj28x4", // Pre-funded account 1
"idx1xygwjmmj8rq3rq3k4adqvhd55x5yqjc8ktcm7e", // Pre-funded account 2
}
}
func (suite *EventEmissionTestSuite) TearDownSuite() {
if suite.cancel != nil {
suite.cancel()
}
}
// TestDIDModuleEventEmissions tests all DID module events
func (suite *EventEmissionTestSuite) TestDIDModuleEventEmissions() {
suite.T().Log("Testing DID module event emissions")
// Get current block height to filter events
startHeight, err := suite.cfg.Client.GetLatestBlockHeight(suite.ctx)
require.NoError(suite.T(), err, "failed to get start height")
suite.T().Run("EventDIDCreated", func(t *testing.T) {
suite.testDIDCreatedEvent(t, startHeight)
})
suite.T().Run("EventVerificationMethodRemoved", func(t *testing.T) {
suite.testVerificationMethodRemovedEvent(t, startHeight)
})
suite.T().Run("EventServiceAdded", func(t *testing.T) {
suite.testServiceAddedEvent(t, startHeight)
})
suite.T().Run("EventServiceRemoved", func(t *testing.T) {
suite.testServiceRemovedEvent(t, startHeight)
})
suite.T().Run("EventWebAuthnRegistered", func(t *testing.T) {
suite.testWebAuthnRegisteredEvent(t, startHeight)
})
suite.T().Run("EventExternalWalletLinked", func(t *testing.T) {
suite.testExternalWalletLinkedEvent(t, startHeight)
})
}
// TestDWNModuleEventEmissions tests all DWN module events
func (suite *EventEmissionTestSuite) TestDWNModuleEventEmissions() {
suite.T().Log("Testing DWN module event emissions")
// Get current block height to filter events
startHeight, err := suite.cfg.Client.GetLatestBlockHeight(suite.ctx)
require.NoError(suite.T(), err, "failed to get start height")
suite.T().Run("EventRecordWritten", func(t *testing.T) {
suite.testRecordWrittenEvent(t, startHeight)
})
suite.T().Run("EventProtocolConfigured", func(t *testing.T) {
suite.testProtocolConfiguredEvent(t, startHeight)
})
suite.T().Run("EventPermissionGranted", func(t *testing.T) {
suite.testPermissionGrantedEvent(t, startHeight)
})
suite.T().Run("EventPermissionRevoked", func(t *testing.T) {
suite.testPermissionRevokedEvent(t, startHeight)
})
suite.T().Run("EventVaultCreated", func(t *testing.T) {
suite.testVaultCreatedEvent(t, startHeight)
})
suite.T().Run("EventVaultKeysRotated", func(t *testing.T) {
suite.testVaultKeysRotatedEvent(t, startHeight)
})
}
// TestEventPersistenceAndReplay tests that events persist and can be replayed
func (suite *EventEmissionTestSuite) TestEventPersistenceAndReplay() {
suite.T().Log("Testing event persistence and replay")
// Record the current height
currentHeight, err := suite.cfg.Client.GetLatestBlockHeight(suite.ctx)
require.NoError(suite.T(), err, "failed to get current height")
// Create a test DID to generate events
testDID := fmt.Sprintf("did:sonr:persistence-test-%d", time.Now().Unix())
txResp := suite.createTestDID(suite.T(), testDID, suite.userAddrs[0])
// Wait for transaction to be included
_, err = suite.cfg.Client.WaitForTx(suite.ctx, txResp.TxHash, 30*time.Second)
require.NoError(suite.T(), err, "failed to wait for transaction")
// Get the block height where the transaction was included
txHeight := txResp.Height
txHeightInt, err := strconv.ParseInt(txHeight, 10, 64)
require.NoError(suite.T(), err, "failed to parse tx height")
suite.T().Run("events_persist_across_queries", func(t *testing.T) {
// Query events by height multiple times to ensure consistency
for i := 0; i < 3; i++ {
blockEvents, err := suite.cfg.Client.QueryEventsByHeight(suite.ctx, txHeightInt)
require.NoError(t, err, "failed to query events by height on attempt %d", i+1)
require.NotNil(t, blockEvents, "block events should not be nil")
// Verify that the same events are returned each time
found := false
for _, txEvents := range blockEvents.TxEvents {
if txEvents.TxHash == txResp.TxHash {
found = true
// Verify DID created event is present (simplified check)
require.NotEmpty(t, txEvents.Events, "transaction should have events")
break
}
}
require.True(t, found, "transaction events should be found in block events")
}
})
suite.T().Run("events_queryable_by_attribute", func(t *testing.T) {
// Query events by DID attribute
events, err := suite.cfg.Client.QueryEventsByAttribute(suite.ctx, "did", testDID, currentHeight, 0)
require.NoError(t, err, "failed to query events by attribute")
// Should find at least the DID creation event
foundDIDEvent := false
for _, event := range events.Events {
if event.Type == "did.v1.EventDIDCreated" {
foundDIDEvent = true
break
}
}
require.True(t, foundDIDEvent, "should find DID created event by attribute query")
})
}
// TestEventQuerying tests various CometBFT query syntax patterns
func (suite *EventEmissionTestSuite) TestEventQuerying() {
suite.T().Log("Testing event querying with CometBFT syntax")
startHeight, err := suite.cfg.Client.GetLatestBlockHeight(suite.ctx)
require.NoError(suite.T(), err, "failed to get start height")
// Create multiple test DIDs for complex querying
testDIDs := []string{
fmt.Sprintf("did:sonr:query-test-1-%d", time.Now().Unix()),
fmt.Sprintf("did:sonr:query-test-2-%d", time.Now().Unix()),
fmt.Sprintf("did:sonr:query-test-3-%d", time.Now().Unix()),
}
var txHashes []string
for i, testDID := range testDIDs {
txResp := suite.createTestDID(suite.T(), testDID, suite.userAddrs[i%len(suite.userAddrs)])
txHashes = append(txHashes, txResp.TxHash)
// Wait for transaction
_, err = suite.cfg.Client.WaitForTx(suite.ctx, txResp.TxHash, 30*time.Second)
require.NoError(suite.T(), err, "failed to wait for transaction %s", txResp.TxHash)
}
endHeight, err := suite.cfg.Client.GetLatestBlockHeight(suite.ctx)
require.NoError(suite.T(), err, "failed to get end height")
suite.T().Run("query_by_event_type", func(t *testing.T) {
events, err := suite.cfg.Client.QueryEventsByType(suite.ctx, "did.v1.EventDIDCreated", startHeight, endHeight)
require.NoError(t, err, "failed to query by event type")
// Should find at least our test events
foundCount := 0
for _, event := range events.Events {
if event.Type == "did.v1.EventDIDCreated" {
foundCount++
}
}
require.GreaterOrEqual(t, foundCount, len(testDIDs), "should find at least %d DID created events", len(testDIDs))
})
suite.T().Run("query_by_creator", func(t *testing.T) {
// Query events by specific creator
events, err := suite.cfg.Client.QueryEventsByAttribute(suite.ctx, "creator", suite.userAddrs[0], startHeight, endHeight)
require.NoError(t, err, "failed to query by creator")
// Should find events created by this user
foundUserEvents := false
for _, event := range events.Events {
if event.Type == "did.v1.EventDIDCreated" {
foundUserEvents = true
break
}
}
require.True(t, foundUserEvents, "should find events created by specific user")
})
suite.T().Run("complex_query_patterns", func(t *testing.T) {
// Test complex query with multiple conditions
query := fmt.Sprintf("message.sender='%s' AND tx.height>=%d", suite.userAddrs[0], startHeight)
events, err := suite.cfg.Client.SearchEvents(suite.ctx, query, startHeight, endHeight)
require.NoError(t, err, "failed to execute complex query")
// Should find some events
require.NotEmpty(t, events.Events, "complex query should return some events")
})
}
// TestMultiEventTransactions tests transactions that emit multiple events
func (suite *EventEmissionTestSuite) TestMultiEventTransactions() {
suite.T().Log("Testing multi-event transactions")
// Create a DID with multiple verification methods and services
// This should emit multiple events in a single transaction
testDID := fmt.Sprintf("did:sonr:multi-event-test-%d", time.Now().Unix())
// For this test, we'll simulate a transaction that creates a DID with services
// which should emit both EventDIDCreated and EventServiceAdded
txResp := suite.createTestDIDWithService(suite.T(), testDID, suite.userAddrs[0])
// Wait for transaction to be included
finalTx, err := suite.cfg.Client.WaitForTx(suite.ctx, txResp.TxHash, 30*time.Second)
require.NoError(suite.T(), err, "failed to wait for transaction")
// Verify multiple events were emitted in the correct order
events := finalTx.TxResponse.Events
require.NotEmpty(suite.T(), events, "transaction should emit events")
// Look for DID creation events
didCreatedEvents := client.FilterEventsByType(events, "EventDIDCreated")
require.NotEmpty(suite.T(), didCreatedEvents, "should emit EventDIDCreated")
// Look for service addition events if services were added
serviceAddedEvents := client.FilterEventsByType(events, "EventServiceAdded")
// Note: This might be empty if the current implementation doesn't emit service events during DID creation
_ = serviceAddedEvents // Avoid unused variable warning
suite.T().Run("events_have_correct_order", func(t *testing.T) {
// Events should be in a logical order
// For DID creation, EventDIDCreated should come before any EventServiceAdded
didCreatedIndex := -1
serviceAddedIndex := -1
for i, event := range events {
if event.Type == "did.v1.EventDIDCreated" {
didCreatedIndex = i
}
if event.Type == "did.v1.EventServiceAdded" {
serviceAddedIndex = i
}
}
require.NotEqual(t, -1, didCreatedIndex, "should find EventDIDCreated")
if serviceAddedIndex != -1 {
require.Less(t, didCreatedIndex, serviceAddedIndex, "EventDIDCreated should come before EventServiceAdded")
}
})
suite.T().Run("events_have_consistent_block_height", func(t *testing.T) {
// All events in the same transaction should have the same block height
expectedHeight := finalTx.TxResponse.Height
for _, event := range events {
// Check if this is one of our custom events
if event.Type == "did.v1.EventDIDCreated" || event.Type == "did.v1.EventServiceAdded" {
// Verify block height attribute if present
if blockHeight, found := client.GetEventAttribute(event, "block_height"); found {
require.Equal(t, expectedHeight, blockHeight, "event block height should match transaction height")
}
}
}
})
}
// TestEventSubscription tests WebSocket event subscription
func (suite *EventEmissionTestSuite) TestEventSubscription() {
suite.T().Log("Testing event subscription via WebSocket")
// Create WebSocket client
wsClient := client.NewWebSocketClient("ws://localhost:26657") // CometBFT WebSocket endpoint
err := wsClient.Connect(suite.ctx)
if err != nil {
suite.T().Skipf("WebSocket connection failed, skipping subscription tests: %v", err)
return
}
defer wsClient.Close()
suite.T().Run("subscribe_to_new_blocks", func(t *testing.T) {
// Subscribe to new block events
subscription, err := wsClient.SubscribeToNewBlockHeaders(suite.ctx)
require.NoError(t, err, "failed to subscribe to new block headers")
defer subscription.Close()
// Wait for at least one block event
event, err := subscription.WaitForEvent(suite.ctx, 30*time.Second, nil)
require.NoError(t, err, "failed to receive block event")
require.NotNil(t, event, "block event should not be nil")
t.Logf("Received block event: %+v", event)
})
suite.T().Run("subscribe_to_tx_events", func(t *testing.T) {
// Subscribe to transaction events
subscription, err := wsClient.SubscribeToTxEvents(suite.ctx)
require.NoError(t, err, "failed to subscribe to transaction events")
defer subscription.Close()
// Create a transaction to trigger an event
testDID := fmt.Sprintf("did:sonr:websocket-test-%d", time.Now().Unix())
txResp := suite.createTestDID(t, testDID, suite.userAddrs[0])
// Wait for the transaction event
event, err := subscription.WaitForEvent(suite.ctx, 30*time.Second, func(event *client.SubscriptionEvent) bool {
// Check if this event relates to our transaction
eventStr := fmt.Sprintf("%v", event.Data.Value)
return strings.Contains(eventStr, txResp.TxHash)
})
if err != nil {
t.Logf("Transaction event subscription test skipped (requires real transactions): %v", err)
} else {
require.NotNil(t, event, "transaction event should not be nil")
t.Logf("Received transaction event: %+v", event)
}
})
suite.T().Run("subscribe_to_did_events", func(t *testing.T) {
// Subscribe to DID-specific events
subscription, err := wsClient.SubscribeToDIDEvents(suite.ctx)
if err != nil {
t.Skipf("DID event subscription failed (may require specific CometBFT configuration): %v", err)
return
}
defer subscription.Close()
// Create a DID to trigger an event
testDID := fmt.Sprintf("did:sonr:did-sub-test-%d", time.Now().Unix())
_ = suite.createTestDID(t, testDID, suite.userAddrs[0])
// Wait for the DID event
event, err := subscription.WaitForEventByType(suite.ctx, 30*time.Second, "EventDIDCreated")
if err != nil {
t.Logf("DID event subscription test skipped (requires real DID transactions): %v", err)
} else {
require.NotNil(t, event, "DID event should not be nil")
t.Logf("Received DID event: %+v", event)
}
})
suite.T().Run("subscribe_with_custom_query", func(t *testing.T) {
// Subscribe to events with a custom query
customQuery := "tx.height > 1"
subscription, err := wsClient.Subscribe(suite.ctx, customQuery)
require.NoError(t, err, "failed to subscribe with custom query")
defer subscription.Close()
// Wait for any event matching the query
event, err := subscription.WaitForEvent(suite.ctx, 30*time.Second, nil)
if err != nil {
t.Logf("Custom query subscription test result: %v", err)
} else {
require.NotNil(t, event, "custom query event should not be nil")
t.Logf("Received custom query event: %+v", event)
}
})
}
// TestEventAttributeValidation tests that event attributes are correctly populated
func (suite *EventEmissionTestSuite) TestEventAttributeValidation() {
suite.T().Log("Testing event attribute validation")
testDID := fmt.Sprintf("did:sonr:attr-test-%d", time.Now().Unix())
creator := suite.userAddrs[0]
// Create a test DID
txResp := suite.createTestDID(suite.T(), testDID, creator)
// Wait for transaction
finalTx, err := suite.cfg.Client.WaitForTx(suite.ctx, txResp.TxHash, 30*time.Second)
require.NoError(suite.T(), err, "failed to wait for transaction")
// Find the DID created event
events := finalTx.TxResponse.Events
didCreatedEvents := client.FilterEventsByType(events, "EventDIDCreated")
require.NotEmpty(suite.T(), didCreatedEvents, "should emit EventDIDCreated")
didEvent := didCreatedEvents[0]
suite.T().Run("required_attributes_present", func(t *testing.T) {
// Check that required attributes are present
requiredAttrs := []string{"did", "creator"}
for _, requiredAttr := range requiredAttrs {
value, found := client.GetEventAttribute(didEvent, requiredAttr)
require.True(t, found, "attribute %s should be present", requiredAttr)
require.NotEmpty(t, value, "attribute %s should not be empty", requiredAttr)
}
})
suite.T().Run("attribute_values_correct", func(t *testing.T) {
// Verify specific attribute values
if didValue, found := client.GetEventAttribute(didEvent, "did"); found {
require.Contains(t, didValue, testDID, "DID attribute should contain test DID")
}
if creatorValue, found := client.GetEventAttribute(didEvent, "creator"); found {
require.Contains(t, creatorValue, creator, "creator attribute should contain creator address")
}
// Check for block height if present
if heightValue, found := client.GetEventAttribute(didEvent, "block_height"); found {
require.NotEmpty(t, heightValue, "block height should not be empty")
require.Equal(t, finalTx.TxResponse.Height, heightValue, "block height should match transaction height")
}
})
}
// Helper methods for creating test transactions
func (suite *EventEmissionTestSuite) createTestDID(t *testing.T, didID, creator string) *client.TxResponse {
// This is a placeholder - in a real implementation, you would:
// 1. Build a proper MsgCreateDID transaction
// 2. Sign it with the creator's key
// 3. Broadcast it to the network
// For now, we'll simulate this by creating a mock transaction response
// In the actual implementation, you would use the actual transaction building logic
t.Logf("Creating test DID: %s by creator: %s", didID, creator)
// Placeholder - replace with actual transaction building and broadcasting
return &client.TxResponse{
TxHash: fmt.Sprintf("mock-tx-%d", time.Now().Unix()),
Code: 0,
Height: fmt.Sprintf("%d", time.Now().Unix()),
Events: []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}{
{
Type: "did.v1.EventDIDCreated",
Attributes: []struct {
Key string `json:"key"`
Value string `json:"value"`
}{
{Key: "did", Value: didID},
{Key: "creator", Value: creator},
{Key: "block_height", Value: fmt.Sprintf("%d", time.Now().Unix())},
},
},
},
}
}
func (suite *EventEmissionTestSuite) createTestDIDWithService(t *testing.T, didID, creator string) *client.TxResponse {
// Similar to createTestDID but includes service creation
t.Logf("Creating test DID with service: %s by creator: %s", didID, creator)
return &client.TxResponse{
TxHash: fmt.Sprintf("mock-tx-with-service-%d", time.Now().Unix()),
Code: 0,
Height: fmt.Sprintf("%d", time.Now().Unix()),
Events: []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}{
{
Type: "did.v1.EventDIDCreated",
Attributes: []struct {
Key string `json:"key"`
Value string `json:"value"`
}{
{Key: "did", Value: didID},
{Key: "creator", Value: creator},
{Key: "block_height", Value: fmt.Sprintf("%d", time.Now().Unix())},
},
},
{
Type: "did.v1.EventServiceAdded",
Attributes: []struct {
Key string `json:"key"`
Value string `json:"value"`
}{
{Key: "did", Value: didID},
{Key: "service_id", Value: didID + "#service-1"},
{Key: "type", Value: "LinkedDomains"},
{Key: "endpoint", Value: "https://example.com"},
},
},
},
}
}
// Individual event test methods
func (suite *EventEmissionTestSuite) testDIDCreatedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventDIDCreated emission")
testDID := fmt.Sprintf("did:sonr:created-test-%d", time.Now().Unix())
txResp := suite.createTestDID(t, testDID, suite.userAddrs[0])
// Verify event was emitted
didEvents := client.FilterEventsByType(txResp.Events, "EventDIDCreated")
require.NotEmpty(t, didEvents, "should emit EventDIDCreated")
// Verify event attributes
didEvent := didEvents[0]
didValue, found := client.GetEventAttribute(didEvent, "did")
require.True(t, found, "DID attribute should be present")
require.Contains(t, didValue, testDID, "DID value should match")
}
func (suite *EventEmissionTestSuite) testVerificationMethodRemovedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventVerificationMethodRemoved emission")
// Implementation would involve:
// 1. Create a DID with verification methods
// 2. Remove a verification method
// 3. Verify EventVerificationMethodRemoved is emitted
t.Skip("Implementation requires actual transaction building - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testServiceAddedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventServiceAdded emission")
// Implementation would involve:
// 1. Create or update a DID to add a service
// 2. Verify EventServiceAdded is emitted with correct attributes
t.Skip("Implementation requires actual transaction building - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testServiceRemovedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventServiceRemoved emission")
t.Skip("Implementation requires actual transaction building - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testWebAuthnRegisteredEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventWebAuthnRegistered emission")
t.Skip("Implementation requires actual WebAuthn transaction building - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testExternalWalletLinkedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventExternalWalletLinked emission")
t.Skip("Implementation requires actual wallet linking transaction - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testRecordWrittenEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventRecordWritten emission")
t.Skip("Implementation requires actual DWN record transaction building - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testProtocolConfiguredEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventProtocolConfigured emission")
t.Skip("Implementation requires actual protocol configuration transaction - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testPermissionGrantedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventPermissionGranted emission")
t.Skip("Implementation requires actual permission granting transaction - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testPermissionRevokedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventPermissionRevoked emission")
t.Skip("Implementation requires actual permission revocation transaction - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testVaultCreatedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventVaultCreated emission")
t.Skip("Implementation requires actual vault creation transaction - placeholder for future implementation")
}
func (suite *EventEmissionTestSuite) testVaultKeysRotatedEvent(t *testing.T, startHeight int64) {
t.Log("Testing EventVaultKeysRotated emission")
t.Skip("Implementation requires actual key rotation transaction - placeholder for future implementation")
}
+124
View File
@@ -0,0 +1,124 @@
package modules
import (
"context"
"net/http"
"testing"
"cosmossdk.io/math"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/test/e2e/utils"
)
func TestSvcModule(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("svc_params", func(t *testing.T) {
// Query service module parameters
url := cfg.BaseURL + "/sonr/svc/v1/params"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
require.NoError(t, err, "failed to create request")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "failed to query svc params")
defer resp.Body.Close()
// Accept 501 Not Implemented for now as the endpoint may not be ready
if resp.StatusCode == http.StatusNotImplemented {
t.Skip("SVC params endpoint not implemented yet")
}
require.Equal(t, http.StatusOK, resp.StatusCode, "svc params query should succeed")
})
t.Run("svc_integration", func(t *testing.T) {
// Use pre-funded account from localnet
testAddr := "idx1fcqk3crpnyvyhtd4jepsnx5eat5ehc920epq29"
// Verify user has balance for service operations
balance, err := cfg.Client.GetBalance(ctx, testAddr, cfg.StakingDenom)
require.NoError(t, err, "failed to query balance")
require.True(t, balance.GT(math.ZeroInt()), "should have balance for operations")
})
}
func TestTokenFactoryModule(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("tokenfactory_params", func(t *testing.T) {
// Query tokenfactory module parameters
url := cfg.BaseURL + "/osmosis/tokenfactory/v1beta1/params"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
require.NoError(t, err, "failed to create request")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "failed to query tokenfactory params")
defer resp.Body.Close()
// Note: This might return 404 if tokenfactory endpoint is different
// or module is not enabled, which is acceptable
if resp.StatusCode != http.StatusNotFound {
require.Equal(t, http.StatusOK, resp.StatusCode, "tokenfactory params query should succeed when available")
}
})
}
func TestDIDModule(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("did_params", func(t *testing.T) {
// Query DID module parameters
url := cfg.BaseURL + "/sonr/did/v1/params"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
require.NoError(t, err, "failed to create request")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "failed to query did params")
defer resp.Body.Close()
// Accept 501 Not Implemented for now as the endpoint may not be ready
if resp.StatusCode == http.StatusNotImplemented {
t.Skip("DID params endpoint not implemented yet")
}
require.Equal(t, http.StatusOK, resp.StatusCode, "did params query should succeed")
})
t.Run("did_functionality", func(t *testing.T) {
// Use pre-funded account from localnet
testAddr := "idx1fcqk3crpnyvyhtd4jepsnx5eat5ehc920epq29"
// Verify user has balance for DID operations
balance, err := cfg.Client.GetBalance(ctx, testAddr, cfg.StakingDenom)
require.NoError(t, err, "failed to query balance")
require.True(t, balance.GT(math.NewInt(1_000_000)), "should have sufficient balance for DID operations")
})
}
func TestDWNModule(t *testing.T) {
cfg := utils.NewTestConfig()
ctx := context.Background()
t.Run("dwn_params", func(t *testing.T) {
// Query DWN module parameters
url := cfg.BaseURL + "/sonr/dwn/v1/params"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
require.NoError(t, err, "failed to create request")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "failed to query dwn params")
defer resp.Body.Close()
// Accept 501 Not Implemented for now as the endpoint may not be ready
if resp.StatusCode == http.StatusNotImplemented {
t.Skip("DWN params endpoint not implemented yet")
}
require.Equal(t, http.StatusOK, resp.StatusCode, "dwn params query should succeed")
})
}
+563
View File
@@ -0,0 +1,563 @@
package e2e
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/sonr-io/sonr/app"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/crypto/ucan"
didtypes "github.com/sonr-io/sonr/x/did/types"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
dwntypes "github.com/sonr-io/sonr/x/dwn/types"
svctypes "github.com/sonr-io/sonr/x/svc/types"
)
// UCANBlockchainE2ETestSuite tests UCAN integration with blockchain operations
type UCANBlockchainE2ETestSuite struct {
suite.Suite
ctx context.Context
app *app.App
clientCtx client.Context
walletPlugin plugin.Plugin
enclaveData *mpc.EnclaveData
userDID string
userAddress types.AccAddress
serviceDID string
ucanToken string
}
func (suite *UCANBlockchainE2ETestSuite) SetupSuite() {
suite.ctx = context.Background()
// Initialize test enclave
suite.enclaveData = &mpc.EnclaveData{
PubHex: "0x04b9e72dfd423bcf95b3801ac93f74ec5ecf47f2cc7d8c5b7a0e4d4e0e3f2a1b2",
PubBytes: make([]byte, 65),
Nonce: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
Curve: mpc.CurveName("secp256k1"),
}
// Initialize test DIDs
suite.userDID = "did:sonr:test-user"
suite.serviceDID = "did:sonr:test-service"
}
// TestE2EGaslessTransactionWithUCAN tests gasless transaction execution with UCAN
func (suite *UCANBlockchainE2ETestSuite) TestE2EGaslessTransactionWithUCAN() {
// Initialize wallet plugin
enclaveBytes, err := json.Marshal(suite.enclaveData)
suite.Require().NoError(err)
suite.walletPlugin, err = plugin.LoadPluginWithEnclave(
suite.ctx,
"sonrtest_1-1",
enclaveBytes,
nil,
)
suite.Require().NoError(err)
// Create UCAN token for gasless transaction
tokenReq := &plugin.NewOriginTokenRequest{
AudienceDID: suite.serviceDID,
Attenuations: []map[string]any{
{
"can": []string{"did/update", "did/deactivate"},
"with": fmt.Sprintf("did://%s", suite.userDID),
},
{
"can": []string{"dwn/write", "dwn/delete"},
"with": fmt.Sprintf("dwn://%s/records", suite.userDID),
},
},
Facts: []string{
"gasless=true",
"transaction_type=did_update",
},
ExpiresAt: time.Now().Add(time.Hour).Unix(),
}
tokenResp, err := suite.walletPlugin.NewOriginToken(tokenReq)
suite.NoError(err, "Should create UCAN token for gasless transaction")
suite.NotEmpty(tokenResp.Token)
suite.ucanToken = tokenResp.Token
// Parse token to verify structure
parsedToken, err := ucan.ParseToken(suite.ucanToken)
suite.NoError(err)
suite.Contains(parsedToken.Facts[0].String(), "gasless=true")
}
// TestE2EDIDOperationsWithUCAN tests DID module operations with UCAN authorization
func (suite *UCANBlockchainE2ETestSuite) TestE2EDIDOperationsWithUCAN() {
// Create UCAN token for DID operations
didTokenReq := &plugin.NewOriginTokenRequest{
AudienceDID: suite.serviceDID,
Attenuations: []map[string]any{
{
"can": []string{
"did/create",
"did/update",
"did/add_verification_method",
"did/remove_verification_method",
},
"with": "did://*", // Allow operations on any DID
},
},
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
tokenResp, err := suite.walletPlugin.NewOriginToken(didTokenReq)
suite.NoError(err)
suite.NotEmpty(tokenResp.Token)
// Simulate DID document creation with UCAN
didDoc := &didtypes.DIDDocument{
Id: suite.userDID,
VerificationMethod: []*didtypes.VerificationMethod{
{
Id: fmt.Sprintf("%s#key-1", suite.userDID),
VerificationMethodKind: "Ed25519VerificationKey2020",
Controller: suite.userDID,
PublicKeyMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
},
},
Authentication: []string{fmt.Sprintf("%s#key-1", suite.userDID)},
}
// Verify UCAN grants permission for DID creation
token, err := ucan.ParseToken(tokenResp.Token)
suite.NoError(err)
hasPermission := false
for _, att := range token.Attenuations {
capability := att.Capability
if capability != nil {
actions := capability.GetActions()
for _, action := range actions {
if action == "did/create" {
hasPermission = true
break
}
}
}
}
suite.True(hasPermission, "UCAN should grant DID creation permission")
// Create delegated token for specific DID
delegatedReq := &plugin.NewAttenuatedTokenRequest{
ParentToken: tokenResp.Token,
AudienceDID: "did:sonr:delegated-controller",
Attenuations: []map[string]any{
{
"can": []string{"did/update"}, // Only update, not create
"with": fmt.Sprintf("did://%s", suite.userDID),
},
},
ExpiresAt: time.Now().Add(12 * time.Hour).Unix(),
}
delegatedResp, err := suite.walletPlugin.NewAttenuatedToken(delegatedReq)
suite.NoError(err)
suite.NotEmpty(delegatedResp.Token)
// Verify delegated token has reduced permissions
delegatedToken, err := ucan.ParseToken(delegatedResp.Token)
suite.NoError(err)
suite.Len(delegatedToken.Attenuations, 1)
// Verify only update permission remains
capability := delegatedToken.Attenuations[0].Capability
actions := capability.GetActions()
suite.Len(actions, 1)
suite.Equal("did/update", actions[0])
}
// TestE2EDWNOperationsWithUCAN tests DWN module operations with UCAN authorization
func (suite *UCANBlockchainE2ETestSuite) TestE2EDWNOperationsWithUCAN() {
// Create UCAN token for DWN operations
dwnTokenReq := &plugin.NewOriginTokenRequest{
AudienceDID: suite.serviceDID,
Attenuations: []map[string]any{
{
"can": []string{
"dwn/write",
"dwn/read",
"dwn/delete",
"dwn/query",
},
"with": fmt.Sprintf("dwn://%s/records/*", suite.userDID),
},
{
"can": []string{"dwn/admin"},
"with": fmt.Sprintf("dwn://%s/permissions", suite.userDID),
},
},
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
tokenResp, err := suite.walletPlugin.NewOriginToken(dwnTokenReq)
suite.NoError(err)
suite.NotEmpty(tokenResp.Token)
// Parse token
token, err := ucan.ParseToken(tokenResp.Token)
suite.NoError(err)
// Simulate DWN record creation
record := &dwntypes.DWNRecord{
ContextId: suite.userDID,
RecordId: "record-123",
Schema: "https://schema.org/TextDigitalDocument",
ParentId: "",
DataCid: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
DateCreated: time.Now().Unix(),
DateModified: time.Now().Unix(),
DatePublished: 0,
Published: false,
EncryptedDataCid: "",
}
// Verify UCAN grants write permission
hasWritePermission := false
for _, att := range token.Attenuations {
capability := att.Capability
if capability != nil {
actions := capability.GetActions()
for _, action := range actions {
if action == "dwn/write" {
hasWritePermission = true
break
}
}
}
}
suite.True(hasWritePermission, "UCAN should grant DWN write permission")
// Create scoped token for specific record
scopedReq := &plugin.NewAttenuatedTokenRequest{
ParentToken: tokenResp.Token,
AudienceDID: "did:sonr:record-processor",
Attenuations: []map[string]any{
{
"can": []string{"dwn/read"}, // Read-only access
"with": fmt.Sprintf("dwn://%s/records/%s", suite.userDID, record.RecordId),
},
},
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}
scopedResp, err := suite.walletPlugin.NewAttenuatedToken(scopedReq)
suite.NoError(err)
suite.NotEmpty(scopedResp.Token)
// Verify scoped permissions
scopedToken, err := ucan.ParseToken(scopedResp.Token)
suite.NoError(err)
suite.Len(scopedToken.Attenuations, 1)
capability := scopedToken.Attenuations[0].Capability
actions := capability.GetActions()
suite.Equal([]string{"dwn/read"}, actions)
}
// TestE2EServiceRegistrationWithUCAN tests service registration with UCAN
func (suite *UCANBlockchainE2ETestSuite) TestE2EServiceRegistrationWithUCAN() {
// Create UCAN token for service operations
svcTokenReq := &plugin.NewOriginTokenRequest{
AudienceDID: suite.serviceDID,
Attenuations: []map[string]any{
{
"can": []string{
"service/register",
"service/update",
"service/deactivate",
},
"with": "service://*",
},
{
"can": []string{"service/verify_domain"},
"with": "service://example.com",
},
},
Facts: []string{
"service_type=oauth2_provider",
"domain=example.com",
},
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(), // 30 days
}
tokenResp, err := suite.walletPlugin.NewOriginToken(svcTokenReq)
suite.NoError(err)
suite.NotEmpty(tokenResp.Token)
// Parse token
token, err := ucan.ParseToken(tokenResp.Token)
suite.NoError(err)
// Simulate service registration
service := &svctypes.Service{
Id: "service-123",
Owner: suite.userDID,
Domain: "example.com",
Description: "Test OAuth2 Provider",
Endpoints: []*svctypes.ServiceEndpoint{
{
Id: "oauth2",
Type: "OAuth2Provider",
ServiceEndpoint: "https://example.com/oauth2",
},
},
}
// Verify registration permission
hasRegisterPermission := false
for _, att := range token.Attenuations {
capability := att.Capability
if capability != nil {
actions := capability.GetActions()
for _, action := range actions {
if action == "service/register" {
hasRegisterPermission = true
break
}
}
}
}
suite.True(hasRegisterPermission, "UCAN should grant service registration permission")
// Create admin token for service management
adminReq := &plugin.NewAttenuatedTokenRequest{
ParentToken: tokenResp.Token,
AudienceDID: "did:sonr:service-admin",
Attenuations: []map[string]any{
{
"can": []string{"service/update", "service/deactivate"},
"with": fmt.Sprintf("service://%s", service.Id),
},
},
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Unix(), // 7 days
}
adminResp, err := suite.walletPlugin.NewAttenuatedToken(adminReq)
suite.NoError(err)
suite.NotEmpty(adminResp.Token)
}
// TestE2ECrossModuleUCANDelegation tests UCAN delegation across modules
func (suite *UCANBlockchainE2ETestSuite) TestE2ECrossModuleUCANDelegation() {
// Create master token with permissions across all modules
masterReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:master-service",
Attenuations: []map[string]any{
// DID permissions
{
"can": []string{"did/create", "did/update"},
"with": "did://*",
},
// DWN permissions
{
"can": []string{"dwn/write", "dwn/read"},
"with": "dwn://*",
},
// Service permissions
{
"can": []string{"service/register"},
"with": "service://*",
},
},
Facts: []string{
"type=cross_module",
"purpose=integration",
},
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
masterResp, err := suite.walletPlugin.NewOriginToken(masterReq)
suite.NoError(err)
suite.NotEmpty(masterResp.Token)
// Create module-specific delegations
// DID-only delegation
didDelegation := &plugin.NewAttenuatedTokenRequest{
ParentToken: masterResp.Token,
AudienceDID: "did:sonr:did-manager",
Attenuations: []map[string]any{
{
"can": []string{"did/update"}, // Remove create permission
"with": fmt.Sprintf("did://%s", suite.userDID),
},
},
ExpiresAt: time.Now().Add(12 * time.Hour).Unix(),
}
didResp, err := suite.walletPlugin.NewAttenuatedToken(didDelegation)
suite.NoError(err)
// DWN-only delegation
dwnDelegation := &plugin.NewAttenuatedTokenRequest{
ParentToken: masterResp.Token,
AudienceDID: "did:sonr:dwn-manager",
Attenuations: []map[string]any{
{
"can": []string{"dwn/read"}, // Read-only
"with": fmt.Sprintf("dwn://%s/records", suite.userDID),
},
},
ExpiresAt: time.Now().Add(6 * time.Hour).Unix(),
}
dwnResp, err := suite.walletPlugin.NewAttenuatedToken(dwnDelegation)
suite.NoError(err)
// Verify each delegation has only its module's permissions
didToken, err := ucan.ParseToken(didResp.Token)
suite.NoError(err)
suite.Len(didToken.Attenuations, 1)
dwnToken, err := ucan.ParseToken(dwnResp.Token)
suite.NoError(err)
suite.Len(dwnToken.Attenuations, 1)
// Verify no cross-contamination of permissions
didCapability := didToken.Attenuations[0].Capability
didActions := didCapability.GetActions()
for _, action := range didActions {
suite.Contains(action, "did/", "DID token should only have DID permissions")
}
dwnCapability := dwnToken.Attenuations[0].Capability
dwnActions := dwnCapability.GetActions()
for _, action := range dwnActions {
suite.Contains(action, "dwn/", "DWN token should only have DWN permissions")
}
}
// TestE2EUCANRevocation tests UCAN token revocation
func (suite *UCANBlockchainE2ETestSuite) TestE2EUCANRevocation() {
// Create revocable token
revocableReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:revocable-service",
Attenuations: []map[string]any{
{
"can": []string{"vault/read", "vault/write"},
"with": "vault://sensitive-data",
},
},
Facts: []string{
"revocable=true",
fmt.Sprintf("revocation_id=%d", time.Now().Unix()),
},
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
tokenResp, err := suite.walletPlugin.NewOriginToken(revocableReq)
suite.NoError(err)
suite.NotEmpty(tokenResp.Token)
// Parse token to get revocation ID
token, err := ucan.ParseToken(tokenResp.Token)
suite.NoError(err)
var revocationID string
for _, fact := range token.Facts {
factStr := fact.String()
if len(factStr) > 13 && factStr[:13] == "revocation_id" {
revocationID = factStr[14:]
break
}
}
suite.NotEmpty(revocationID, "Should have revocation ID")
// Simulate revocation list check
revocationList := map[string]bool{
revocationID: false, // Not revoked initially
}
suite.False(revocationList[revocationID], "Token should not be revoked initially")
// Simulate revocation
revocationList[revocationID] = true
suite.True(revocationList[revocationID], "Token should be marked as revoked")
}
// TestE2EBatchUCANOperations tests batch UCAN token operations
func (suite *UCANBlockchainE2ETestSuite) TestE2EBatchUCANOperations() {
const batchSize = 5
tokens := make([]string, 0, batchSize)
// Create batch of tokens
for i := 0; i < batchSize; i++ {
req := &plugin.NewOriginTokenRequest{
AudienceDID: fmt.Sprintf("did:sonr:batch-service-%d", i),
Attenuations: []map[string]any{
{
"can": []string{fmt.Sprintf("batch/operation-%d", i)},
"with": fmt.Sprintf("batch://resource-%d", i),
},
},
Facts: []string{
fmt.Sprintf("batch_index=%d", i),
fmt.Sprintf("batch_id=%d", time.Now().Unix()),
},
ExpiresAt: time.Now().Add(time.Hour).Unix(),
}
resp, err := suite.walletPlugin.NewOriginToken(req)
suite.NoError(err)
suite.NotEmpty(resp.Token)
tokens = append(tokens, resp.Token)
}
suite.Len(tokens, batchSize, "Should create all batch tokens")
// Verify each token
for i, tokenStr := range tokens {
token, err := ucan.ParseToken(tokenStr)
suite.NoError(err)
suite.Equal(fmt.Sprintf("did:sonr:batch-service-%d", i), token.Audience)
// Verify batch-specific permissions
suite.Len(token.Attenuations, 1)
capability := token.Attenuations[0].Capability
actions := capability.GetActions()
suite.Contains(actions[0], fmt.Sprintf("operation-%d", i))
}
// Create aggregated delegation from all batch tokens
aggregatedReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:batch-aggregator",
Attenuations: []map[string]any{
{
"can": []string{"batch/aggregate", "batch/process"},
"with": "batch://*",
},
},
Facts: []string{
fmt.Sprintf("aggregated_count=%d", batchSize),
"type=batch_aggregation",
},
ExpiresAt: time.Now().Add(2 * time.Hour).Unix(),
}
aggregatedResp, err := suite.walletPlugin.NewOriginToken(aggregatedReq)
suite.NoError(err)
suite.NotEmpty(aggregatedResp.Token)
// Verify aggregated token
aggregatedToken, err := ucan.ParseToken(aggregatedResp.Token)
suite.NoError(err)
suite.Contains(aggregatedToken.Facts[0].String(), fmt.Sprintf("aggregated_count=%d", batchSize))
}
func TestUCANBlockchainE2ESuite(t *testing.T) {
suite.Run(t, new(UCANBlockchainE2ETestSuite))
}
+467
View File
@@ -0,0 +1,467 @@
package e2e
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// UCANWalletE2ETestSuite tests end-to-end UCAN operations with wallet functionality
type UCANWalletE2ETestSuite struct {
suite.Suite
ctx context.Context
chainID string
enclaveData *mpc.EnclaveData
walletPlugin plugin.Plugin
issuerDID string
audienceDID string
vaultConfig map[string]any
originToken string
delegatedToken string
}
func (suite *UCANWalletE2ETestSuite) SetupSuite() {
suite.ctx = context.Background()
suite.chainID = "sonrtest_1-1"
suite.audienceDID = "did:sonr:test-audience"
// Initialize test enclave data
suite.enclaveData = &mpc.EnclaveData{
PubHex: "0x04b9e72dfd423bcf95b3801ac93f74ec5ecf47f2cc7d8c5b7a0e4d4e0e3f2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4",
PubBytes: []byte{4, 185, 231, 45, 253, 66, 59, 207, 149, 179, 128, 26, 201, 63, 116, 236, 94, 207, 71, 242, 204, 125, 140, 91, 122, 14, 77, 78, 14, 63, 42, 27, 44, 61, 78, 95, 106, 123, 140, 157, 14, 31, 42, 59, 76, 93, 110, 127, 138, 155, 12, 29, 46, 63, 74, 91, 108, 125, 142, 159, 10, 27, 44, 61, 78},
Nonce: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
Curve: mpc.CurveName("secp256k1"),
}
// Vault configuration for wallet
suite.vaultConfig = map[string]any{
"enclave_type": "mpc",
"threshold": 2,
"parties": 3,
"network": "testnet",
}
}
func (suite *UCANWalletE2ETestSuite) SetupTest() {
// Initialize plugin with enclave for each test
enclaveBytes, err := json.Marshal(suite.enclaveData)
suite.Require().NoError(err, "Failed to marshal enclave data")
// Load plugin with enclave and vault config
suite.walletPlugin, err = plugin.LoadPluginWithEnclave(
suite.ctx,
suite.chainID,
enclaveBytes,
suite.vaultConfig,
)
suite.Require().NoError(err, "Failed to load plugin with enclave")
// Get issuer DID from the wallet
issuerResp, err := suite.walletPlugin.GetIssuerDID()
suite.Require().NoError(err, "Failed to get issuer DID")
suite.issuerDID = issuerResp.DID
}
// TestE2EWalletInitialization tests wallet initialization with EnclaveData
func (suite *UCANWalletE2ETestSuite) TestE2EWalletInitialization() {
// Verify wallet is initialized
suite.NotNil(suite.walletPlugin, "Wallet plugin should be initialized")
// Verify issuer DID is generated
suite.NotEmpty(suite.issuerDID, "Issuer DID should be generated")
suite.Contains(suite.issuerDID, "did:sonr:", "Issuer DID should have correct format")
// Get issuer info again to verify consistency
issuerResp, err := suite.walletPlugin.GetIssuerDID()
suite.NoError(err, "Should retrieve issuer DID")
suite.Equal(suite.issuerDID, issuerResp.DID, "Issuer DID should be consistent")
suite.NotEmpty(issuerResp.Address, "Address should be generated")
suite.NotEmpty(issuerResp.ChainCode, "Chain code should be generated")
}
// TestE2ECreateOriginUCANToken tests creating an origin UCAN token
func (suite *UCANWalletE2ETestSuite) TestE2ECreateOriginUCANToken() {
// Create origin token request
req := &plugin.NewOriginTokenRequest{
AudienceDID: suite.audienceDID,
Attenuations: []map[string]any{
{
"can": []string{"vault/read", "vault/write"},
"with": "vault://user-vault",
},
{
"can": []string{"dwn/read"},
"with": "dwn://user-records",
},
},
Facts: []string{
"origin=test-wallet",
"purpose=e2e-testing",
},
NotBefore: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
// Create origin token
resp, err := suite.walletPlugin.NewOriginToken(req)
suite.NoError(err, "Should create origin token")
suite.NotEmpty(resp.Token, "Token should be generated")
suite.Equal(suite.issuerDID, resp.Issuer, "Issuer should match wallet DID")
suite.NotEmpty(resp.Address, "Address should be included")
suite.Empty(resp.Error, "Should not have error")
// Store for later tests
suite.originToken = resp.Token
// Verify token can be parsed
parsedToken, err := ucan.ParseToken(resp.Token)
suite.NoError(err, "Should parse generated token")
suite.Equal(suite.issuerDID, parsedToken.Issuer, "Parsed issuer should match")
suite.Equal(suite.audienceDID, parsedToken.Audience, "Parsed audience should match")
}
// TestE2ECreateDelegatedUCANToken tests creating a delegated UCAN token
func (suite *UCANWalletE2ETestSuite) TestE2ECreateDelegatedUCANToken() {
// First create an origin token
suite.TestE2ECreateOriginUCANToken()
suite.Require().NotEmpty(suite.originToken, "Origin token required for delegation")
// Create delegated token request with attenuated permissions
req := &plugin.NewAttenuatedTokenRequest{
ParentToken: suite.originToken,
AudienceDID: "did:sonr:delegated-service",
Attenuations: []map[string]any{
{
"can": []string{"vault/read"}, // Reduced from read/write
"with": "vault://user-vault",
},
// Removed dwn permissions - further attenuation
},
Facts: []string{
"delegation=level-1",
"delegator=test-wallet",
},
NotBefore: time.Now().Unix(),
ExpiresAt: time.Now().Add(12 * time.Hour).Unix(), // Shorter expiration
}
// Create delegated token
resp, err := suite.walletPlugin.NewAttenuatedToken(req)
suite.NoError(err, "Should create delegated token")
suite.NotEmpty(resp.Token, "Delegated token should be generated")
suite.Equal(suite.issuerDID, resp.Issuer, "Issuer should be original wallet")
suite.Empty(resp.Error, "Should not have error")
// Store for later tests
suite.delegatedToken = resp.Token
// Verify delegation chain
delegatedParsed, err := ucan.ParseToken(resp.Token)
suite.NoError(err, "Should parse delegated token")
suite.Equal("did:sonr:delegated-service", delegatedParsed.Audience)
suite.NotEmpty(delegatedParsed.Proofs, "Should have proof chain")
}
// TestE2ESignAndVerifyData tests signing and verifying data with the wallet
func (suite *UCANWalletE2ETestSuite) TestE2ESignAndVerifyData() {
testData := []byte("test message for signing")
// Sign data
signReq := &plugin.SignDataRequest{
Data: testData,
}
signResp, err := suite.walletPlugin.SignData(signReq)
suite.NoError(err, "Should sign data")
suite.NotEmpty(signResp.Signature, "Signature should be generated")
suite.Empty(signResp.Error, "Should not have error")
// Verify signature
verifyReq := &plugin.VerifyDataRequest{
Data: testData,
Signature: signResp.Signature,
}
verifyResp, err := suite.walletPlugin.VerifyData(verifyReq)
suite.NoError(err, "Should verify signature")
suite.True(verifyResp.Valid, "Signature should be valid")
suite.Empty(verifyResp.Error, "Should not have error")
// Verify with wrong data should fail
wrongData := []byte("different message")
wrongVerifyReq := &plugin.VerifyDataRequest{
Data: wrongData,
Signature: signResp.Signature,
}
wrongVerifyResp, err := suite.walletPlugin.VerifyData(wrongVerifyReq)
suite.NoError(err, "Should handle verification")
suite.False(wrongVerifyResp.Valid, "Signature should be invalid for wrong data")
}
// TestE2EUCANTokenChainValidation tests validation of UCAN delegation chains
func (suite *UCANWalletE2ETestSuite) TestE2EUCANTokenChainValidation() {
// Create origin token
suite.TestE2ECreateOriginUCANToken()
suite.Require().NotEmpty(suite.originToken)
// Create first delegation
firstDelegation := &plugin.NewAttenuatedTokenRequest{
ParentToken: suite.originToken,
AudienceDID: "did:sonr:service-a",
Attenuations: []map[string]any{
{
"can": []string{"vault/read", "vault/write"},
"with": "vault://user-vault",
},
},
ExpiresAt: time.Now().Add(20 * time.Hour).Unix(),
}
firstResp, err := suite.walletPlugin.NewAttenuatedToken(firstDelegation)
suite.NoError(err, "Should create first delegation")
suite.NotEmpty(firstResp.Token)
// Create second delegation from first
secondDelegation := &plugin.NewAttenuatedTokenRequest{
ParentToken: firstResp.Token,
AudienceDID: "did:sonr:service-b",
Attenuations: []map[string]any{
{
"can": []string{"vault/read"}, // Further attenuated
"with": "vault://user-vault",
},
},
ExpiresAt: time.Now().Add(10 * time.Hour).Unix(),
}
secondResp, err := suite.walletPlugin.NewAttenuatedToken(secondDelegation)
suite.NoError(err, "Should create second delegation")
suite.NotEmpty(secondResp.Token)
// Parse and validate chain
finalToken, err := ucan.ParseToken(secondResp.Token)
suite.NoError(err, "Should parse final token")
suite.Len(finalToken.Proofs, 2, "Should have full proof chain")
// Verify attenuation is properly reduced
suite.Len(finalToken.Attenuations, 1, "Should have one attenuation")
capability := finalToken.Attenuations[0].Capability
suite.NotNil(capability, "Should have capability")
}
// TestE2EWalletRecovery tests wallet recovery from EnclaveData
func (suite *UCANWalletE2ETestSuite) TestE2EWalletRecovery() {
// Get original issuer info
originalIssuer, err := suite.walletPlugin.GetIssuerDID()
suite.NoError(err)
// Create a token with original wallet
tokenReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:recovery-test",
Attenuations: []map[string]any{
{"can": []string{"test"}, "with": "test://resource"},
},
ExpiresAt: time.Now().Add(time.Hour).Unix(),
}
originalResp, err := suite.walletPlugin.NewOriginToken(tokenReq)
suite.NoError(err)
suite.NotEmpty(originalResp.Token)
// Simulate wallet recovery - create new plugin with same enclave data
enclaveBytes, err := json.Marshal(suite.enclaveData)
suite.NoError(err)
recoveredPlugin, err := plugin.LoadPluginWithEnclave(
suite.ctx,
suite.chainID,
enclaveBytes,
suite.vaultConfig,
)
suite.NoError(err, "Should recover wallet from enclave data")
// Verify recovered wallet has same identity
recoveredIssuer, err := recoveredPlugin.GetIssuerDID()
suite.NoError(err)
suite.Equal(originalIssuer.DID, recoveredIssuer.DID, "Recovered DID should match")
suite.Equal(originalIssuer.Address, recoveredIssuer.Address, "Recovered address should match")
// Verify recovered wallet can create compatible tokens
recoveredResp, err := recoveredPlugin.NewOriginToken(tokenReq)
suite.NoError(err)
suite.NotEmpty(recoveredResp.Token)
suite.Equal(originalIssuer.DID, recoveredResp.Issuer, "Issuer should be consistent")
}
// TestE2EMultiPartyWallet tests multi-party wallet operations
func (suite *UCANWalletE2ETestSuite) TestE2EMultiPartyWallet() {
// Configure multi-party vault
multiPartyConfig := map[string]any{
"enclave_type": "mpc",
"threshold": 2,
"parties": 3,
"party_ids": []string{"party-1", "party-2", "party-3"},
"network": "testnet",
}
// Create wallet with multi-party config
enclaveBytes, err := json.Marshal(suite.enclaveData)
suite.NoError(err)
multiPartyPlugin, err := plugin.LoadPluginWithEnclave(
suite.ctx,
suite.chainID,
enclaveBytes,
multiPartyConfig,
)
suite.NoError(err, "Should create multi-party wallet")
// Create token requiring multi-party consensus
consensusReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:multi-party-service",
Attenuations: []map[string]any{
{
"can": []string{"vault/admin", "vault/transfer"},
"with": "vault://multi-party-vault",
},
},
Facts: []string{
"type=multi-party",
"threshold=2/3",
},
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
resp, err := multiPartyPlugin.NewOriginToken(consensusReq)
suite.NoError(err, "Should create multi-party token")
suite.NotEmpty(resp.Token, "Multi-party token should be generated")
// Verify token contains multi-party facts
parsedToken, err := ucan.ParseToken(resp.Token)
suite.NoError(err)
suite.NotEmpty(parsedToken.Facts, "Should have facts")
}
// TestE2EPermissionBoundaries tests UCAN permission boundaries
func (suite *UCANWalletE2ETestSuite) TestE2EPermissionBoundaries() {
// Create token with specific permissions
boundedReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:bounded-service",
Attenuations: []map[string]any{
{
"can": []string{"vault/read"},
"with": "vault://user-vault/documents/*",
},
{
"can": []string{"dwn/write"},
"with": "dwn://user-records/profile",
},
},
ExpiresAt: time.Now().Add(time.Hour).Unix(),
}
resp, err := suite.walletPlugin.NewOriginToken(boundedReq)
suite.NoError(err)
suite.NotEmpty(resp.Token)
// Parse and verify permissions
token, err := ucan.ParseToken(resp.Token)
suite.NoError(err)
suite.Len(token.Attenuations, 2, "Should have two permission sets")
// Verify permission boundaries are maintained
for _, att := range token.Attenuations {
capability := att.Capability
suite.NotNil(capability)
actions := capability.GetActions()
suite.NotEmpty(actions, "Should have actions")
// Verify no escalation beyond original permissions
for _, action := range actions {
suite.Contains([]string{"vault/read", "dwn/write"}, action,
"Action should be within bounded permissions")
}
}
}
// TestE2ETokenExpiration tests UCAN token expiration
func (suite *UCANWalletE2ETestSuite) TestE2ETokenExpiration() {
// Create token with very short expiration
shortLivedReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:short-lived",
Attenuations: []map[string]any{
{"can": []string{"test"}, "with": "test://resource"},
},
ExpiresAt: time.Now().Add(100 * time.Millisecond).Unix(),
}
resp, err := suite.walletPlugin.NewOriginToken(shortLivedReq)
suite.NoError(err)
suite.NotEmpty(resp.Token)
// Parse immediately - should be valid
token, err := ucan.ParseToken(resp.Token)
suite.NoError(err)
suite.NotNil(token)
// Wait for expiration
time.Sleep(200 * time.Millisecond)
// Verify token is expired
now := time.Now().Unix()
suite.Less(token.ExpiresAt, now, "Token should be expired")
}
// TestE2EConcurrentWalletOperations tests concurrent wallet operations
func (suite *UCANWalletE2ETestSuite) TestE2EConcurrentWalletOperations() {
const numOperations = 10
results := make(chan *plugin.UCANTokenResponse, numOperations)
errors := make(chan error, numOperations)
// Launch concurrent token creation
for i := 0; i < numOperations; i++ {
go func(index int) {
req := &plugin.NewOriginTokenRequest{
AudienceDID: fmt.Sprintf("did:sonr:concurrent-%d", index),
Attenuations: []map[string]any{
{"can": []string{"test"}, "with": fmt.Sprintf("test://resource-%d", index)},
},
ExpiresAt: time.Now().Add(time.Hour).Unix(),
}
resp, err := suite.walletPlugin.NewOriginToken(req)
if err != nil {
errors <- err
} else {
results <- resp
}
}(i)
}
// Collect results
successCount := 0
for i := 0; i < numOperations; i++ {
select {
case resp := <-results:
suite.NotEmpty(resp.Token, "Concurrent token should be generated")
suite.Equal(suite.issuerDID, resp.Issuer, "Issuer should be consistent")
successCount++
case err := <-errors:
suite.NoError(err, "Concurrent operation should not fail")
case <-time.After(5 * time.Second):
suite.Fail("Concurrent operation timed out")
}
}
suite.Equal(numOperations, successCount, "All concurrent operations should succeed")
}
func TestUCANWalletE2ESuite(t *testing.T) {
suite.Run(t, new(UCANWalletE2ETestSuite))
}
+142
View File
@@ -0,0 +1,142 @@
package utils
import (
"context"
"testing"
"time"
"cosmossdk.io/math"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/test/e2e/client"
)
// TestConfig holds common test configuration
type TestConfig struct {
ChainID string
BaseURL string
FaucetURL string
StakingDenom string
NormalDenom string
Client *client.StarshipClient
FaucetClient *FaucetClient
DefaultTimeout time.Duration
BlockTime time.Duration
}
// NewTestConfig creates a new test configuration
func NewTestConfig() *TestConfig {
return &TestConfig{
ChainID: "sonrtest_1-1",
BaseURL: "http://localhost:1317",
FaucetURL: "http://localhost:8000",
StakingDenom: "usnr",
NormalDenom: "snr",
DefaultTimeout: 30 * time.Second,
BlockTime: 2 * time.Second,
Client: client.NewStarshipClient("http://localhost:1317"),
FaucetClient: NewFaucetClient("http://localhost:8000"),
}
}
// AssertBalance asserts that an account has the expected balance
func AssertBalance(t *testing.T, cfg *TestConfig, address, denom string, expectedAmount math.Int) {
ctx, cancel := context.WithTimeout(context.Background(), cfg.DefaultTimeout)
defer cancel()
balance, err := cfg.Client.GetBalance(ctx, address, denom)
require.NoError(t, err, "failed to query balance")
require.True(t, balance.Equal(expectedAmount),
"expected balance %s, got %s", expectedAmount.String(), balance.String())
}
// AssertBalanceGreaterThan asserts that an account balance is greater than expected
func AssertBalanceGreaterThan(t *testing.T, cfg *TestConfig, address, denom string, minAmount math.Int) {
ctx, cancel := context.WithTimeout(context.Background(), cfg.DefaultTimeout)
defer cancel()
balance, err := cfg.Client.GetBalance(ctx, address, denom)
require.NoError(t, err, "failed to query balance")
require.True(t, balance.GT(minAmount),
"expected balance > %s, got %s", minAmount.String(), balance.String())
}
// AssertBalanceLessThan asserts that an account balance is less than expected
func AssertBalanceLessThan(t *testing.T, cfg *TestConfig, address, denom string, maxAmount math.Int) {
ctx, cancel := context.WithTimeout(context.Background(), cfg.DefaultTimeout)
defer cancel()
balance, err := cfg.Client.GetBalance(ctx, address, denom)
require.NoError(t, err, "failed to query balance")
require.True(t, balance.LT(maxAmount),
"expected balance < %s, got %s", maxAmount.String(), balance.String())
}
// AssertSupply asserts that a denomination has the expected total supply
func AssertSupply(t *testing.T, cfg *TestConfig, denom string, expectedSupply math.Int) {
ctx, cancel := context.WithTimeout(context.Background(), cfg.DefaultTimeout)
defer cancel()
supply, err := cfg.Client.GetSupply(ctx, denom)
require.NoError(t, err, "failed to query supply")
require.True(t, supply.Equal(expectedSupply),
"expected supply %s, got %s", expectedSupply.String(), supply.String())
}
// AssertTransferChannelExists asserts that an open transfer channel exists
func AssertTransferChannelExists(t *testing.T, cfg *TestConfig) string {
ctx, cancel := context.WithTimeout(context.Background(), cfg.DefaultTimeout)
defer cancel()
channelID, err := cfg.Client.GetTransferChannel(ctx)
require.NoError(t, err, "failed to find transfer channel")
require.NotEmpty(t, channelID, "transfer channel ID should not be empty")
return channelID
}
// WaitForBlocks waits for a specified number of blocks
func WaitForBlocks(ctx context.Context, cfg *TestConfig, blocks int) error {
waitTime := time.Duration(blocks) * cfg.BlockTime
select {
case <-time.After(waitTime):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// AssertNodeInfo asserts basic node information
func AssertNodeInfo(t *testing.T, cfg *TestConfig, expectedNetwork string) {
ctx, cancel := context.WithTimeout(context.Background(), cfg.DefaultTimeout)
defer cancel()
nodeInfo, err := cfg.Client.GetNodeInfo(ctx)
require.NoError(t, err, "failed to query node info")
require.Equal(t, expectedNetwork, nodeInfo.DefaultNodeInfo.Network,
"unexpected network ID")
require.NotEmpty(t, nodeInfo.ApplicationVersion.Version,
"application version should not be empty")
}
// SetupTestUsers creates and funds test users
func SetupTestUsers(t *testing.T, cfg *TestConfig, fundAmount math.Int) []CreateTestUser {
users := GetDefaultTestUsers(fundAmount, cfg.NormalDenom)
ctx, cancel := context.WithTimeout(context.Background(), cfg.DefaultTimeout)
defer cancel()
err := cfg.FaucetClient.FundTestUsers(ctx, users)
require.NoError(t, err, "failed to fund test users")
// Wait for funding transactions to be included
err = WaitForBlocks(ctx, cfg, 2)
require.NoError(t, err, "failed to wait for blocks")
// Verify funding
for _, user := range users {
AssertBalance(t, cfg, user.Address, user.Denom, user.Amount)
}
return users
}
+157
View File
@@ -0,0 +1,157 @@
package utils
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// FaucetClient provides HTTP client for Starship faucet API
type FaucetClient struct {
baseURL string
httpClient *http.Client
}
// NewFaucetClient creates a new faucet HTTP client
func NewFaucetClient(baseURL string) *FaucetClient {
return &FaucetClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// FundRequest represents faucet funding request
type FundRequest struct {
Address string `json:"address"`
Coins []string `json:"coins"`
}
// FundResponse represents faucet funding response
type FundResponse struct {
Status string `json:"status"`
TxHash string `json:"tx_hash,omitempty"`
Error string `json:"error,omitempty"`
}
// FundAccount requests tokens from the faucet for an account
func (f *FaucetClient) FundAccount(ctx context.Context, address string, coins []sdk.Coin) (*FundResponse, error) {
url := fmt.Sprintf("%s/credit", f.baseURL)
// Convert coins to string format
coinStrs := make([]string, len(coins))
for i, coin := range coins {
coinStrs[i] = coin.String()
}
reqBody := FundRequest{
Address: address,
Coins: coinStrs,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal fund request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create fund request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := f.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fund account: %w", err)
}
defer resp.Body.Close()
var fundResp FundResponse
if err := json.NewDecoder(resp.Body).Decode(&fundResp); err != nil {
return nil, fmt.Errorf("failed to decode fund response: %w", err)
}
if fundResp.Status != "success" {
return nil, fmt.Errorf("faucet funding failed: %s", fundResp.Error)
}
return &fundResp, nil
}
// FundAccountWithRetry funds an account with retry logic
func (f *FaucetClient) FundAccountWithRetry(ctx context.Context, address string, coins []sdk.Coin, maxRetries int) error {
const retryDelay = 3 * time.Second
for attempt := 0; attempt < maxRetries; attempt++ {
_, err := f.FundAccount(ctx, address, coins)
if err == nil {
return nil
}
if attempt == maxRetries-1 {
return fmt.Errorf("failed to fund account after %d attempts: %w", maxRetries, err)
}
time.Sleep(retryDelay)
}
return fmt.Errorf("unreachable code")
}
// CreateTestUser represents a test user with funding
type CreateTestUser struct {
Address string
Amount math.Int
Denom string
}
// GetDefaultTestUsers returns default test users with addresses from Starship config
func GetDefaultTestUsers(amount math.Int, denom string) []CreateTestUser {
return []CreateTestUser{
{
Address: "idx13a6zjh96w9z9y2defkktdc6vn4r5h3s7jwxuam", // acc0 from Starship config
Amount: amount,
Denom: denom,
},
{
Address: "idx1xehj0xc24k2c740jslfyd4d6mt8c4dczgntqhg", // acc1 from Starship config
Amount: amount,
Denom: denom,
},
{
Address: "idx1jyq30438zx0g4urancle25r6tk5td6pgeytpfu", // user0 from Starship config
Amount: amount,
Denom: denom,
},
{
Address: "idx1wz5qn36kdakkqunkvwuuvpr2l4amd7y0m3qdq6", // user1 from Starship config
Amount: amount,
Denom: denom,
},
}
}
// FundTestUsers funds multiple test users
func (f *FaucetClient) FundTestUsers(ctx context.Context, users []CreateTestUser) error {
for _, user := range users {
coins := []sdk.Coin{
{
Denom: user.Denom,
Amount: user.Amount,
},
}
if err := f.FundAccountWithRetry(ctx, user.Address, coins, 3); err != nil {
return fmt.Errorf("failed to fund user %s: %w", user.Address, err)
}
}
return nil
}
+458
View File
@@ -0,0 +1,458 @@
package e2e
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/zeebo/blake3"
"github.com/sonr-io/sonr/app"
didtypes "github.com/sonr-io/sonr/x/did/types"
dwntypes "github.com/sonr-io/sonr/x/dwn/types"
)
// WebAuthnRegistrationTestSuite tests the complete WebAuthn registration flow
type WebAuthnRegistrationTestSuite struct {
suite.Suite
cfg network.Config
network *network.Network
}
func (suite *WebAuthnRegistrationTestSuite) SetupSuite() {
suite.T().Log("setting up WebAuthn registration test suite")
cfg := network.DefaultConfig()
cfg.NumValidators = 1
// Custom app constructor
cfg.AppConstructor = func(val network.Validator) app.TestApp {
return app.NewTestApp(val.Ctx.Logger, val.Ctx.Config.DBDir(), nil, true, 0)
}
suite.cfg = cfg
suite.network = network.New(suite.T(), cfg)
suite.Require().NotNil(suite.network)
// Wait for network to start
time.Sleep(5 * time.Second)
}
func (suite *WebAuthnRegistrationTestSuite) TearDownSuite() {
suite.T().Log("tearing down WebAuthn registration test suite")
suite.network.Cleanup()
}
// TestRegisterStart tests the RegisterStart query handler
func (suite *WebAuthnRegistrationTestSuite) TestRegisterStart() {
val := suite.network.Validators[0]
testCases := []struct {
name string
assertionValue string
assertionType string
serviceOrigin string
expectErr bool
errMsg string
}{
{
name: "valid email registration",
assertionValue: "alice@example.com",
assertionType: "email",
serviceOrigin: "http://localhost:3000",
expectErr: false,
},
{
name: "valid phone registration",
assertionValue: "+1234567890",
assertionType: "tel",
serviceOrigin: "http://localhost:3000",
expectErr: false,
},
{
name: "invalid assertion type",
assertionValue: "alice",
assertionType: "invalid",
serviceOrigin: "http://localhost:3000",
expectErr: true,
errMsg: "unsupported assertion type",
},
{
name: "invalid origin",
assertionValue: "alice@example.com",
assertionType: "email",
serviceOrigin: "http://malicious.com",
expectErr: true,
errMsg: "invalid origin",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Create RegisterStart request
req := &didtypes.QueryRegisterStartRequest{
AssertionValue: tc.assertionValue,
AssertionType: tc.assertionType,
ServiceOrigin: tc.serviceOrigin,
}
// Query RegisterStart
clientCtx := val.ClientCtx
queryClient := didtypes.NewQueryClient(clientCtx)
resp, err := queryClient.RegisterStart(context.Background(), req)
if tc.expectErr {
suite.Require().Error(err)
suite.Require().Contains(err.Error(), tc.errMsg)
} else {
suite.Require().NoError(err)
suite.Require().NotNil(resp)
suite.Require().NotNil(resp.DIDDocument)
suite.Require().NotEmpty(resp.DIDDocument.Id)
// Verify challenge is generated
suite.Require().NotNil(resp.DIDDocumentMetadata)
// Note: In real implementation, challenge would be in metadata
}
})
}
}
// TestWebAuthnCredentialRegistration tests the complete registration flow
func (suite *WebAuthnRegistrationTestSuite) TestWebAuthnCredentialRegistration() {
val := suite.network.Validators[0]
// Test data
username := "testuser"
email := "testuser@example.com"
// Generate mock WebAuthn credential data
credentialID := base64.StdEncoding.EncodeToString([]byte("test-credential-id"))
publicKey := generateMockPublicKey()
attestationObject := generateMockAttestationObject()
clientDataJSON := generateMockClientDataJSON()
// Create registration message
msg := &didtypes.MsgRegisterWebAuthnCredential{
Creator: val.Address.String(),
Username: username,
AssertionValue: email,
AssertionType: "email",
WebauthnCredential: &didtypes.WebAuthnCredential{
CredentialId: credentialID,
PublicKey: publicKey,
AttestationObject: attestationObject,
ClientDataJson: clientDataJSON,
},
CreateVault: true,
}
// Broadcast transaction
clientCtx := val.ClientCtx
txResp, err := broadcastTx(clientCtx, msg)
suite.Require().NoError(err)
suite.Require().Equal(uint32(0), txResp.Code)
// Wait for transaction to be processed
time.Sleep(2 * time.Second)
// Verify DID document was created
queryClient := didtypes.NewQueryClient(clientCtx)
// Calculate expected DID
hasher := blake3.New()
hasher.Write([]byte(email))
hash := hasher.Sum(nil)
expectedDID := fmt.Sprintf("did:email:%x", hash)
// Query for the DID document
didResp, err := queryClient.GetDIDDocument(context.Background(), &didtypes.QueryGetDIDDocumentRequest{
Did: expectedDID,
})
suite.Require().NoError(err)
suite.Require().NotNil(didResp)
suite.Require().NotNil(didResp.DidDocument)
suite.Require().Equal(expectedDID, didResp.DidDocument.Id)
// Verify assertion methods
suite.Require().Len(didResp.DidDocument.AssertionMethod, 2)
// Verify authentication method (WebAuthn)
suite.Require().Len(didResp.DidDocument.Authentication, 1)
// Verify controller is set
suite.Require().Len(didResp.DidDocument.Controller, 1)
}
// TestVaultCreation tests that vault is created during registration
func (suite *WebAuthnRegistrationTestSuite) TestVaultCreation() {
val := suite.network.Validators[0]
username := "vaultuser"
email := "vaultuser@example.com"
// Create registration with vault
msg := &didtypes.MsgRegisterWebAuthnCredential{
Creator: val.Address.String(),
Username: username,
AssertionValue: email,
AssertionType: "email",
WebauthnCredential: &didtypes.WebAuthnCredential{
CredentialId: "vault-cred-id",
PublicKey: generateMockPublicKey(),
AttestationObject: generateMockAttestationObject(),
ClientDataJson: generateMockClientDataJSON(),
},
CreateVault: true,
}
clientCtx := val.ClientCtx
txResp, err := broadcastTx(clientCtx, msg)
suite.Require().NoError(err)
suite.Require().Equal(uint32(0), txResp.Code)
// Wait for processing
time.Sleep(2 * time.Second)
// Query vault
dwnQueryClient := dwntypes.NewQueryClient(clientCtx)
// Calculate expected vault ID (based on DID)
hasher := blake3.New()
hasher.Write([]byte(email))
hash := hasher.Sum(nil)
expectedDID := fmt.Sprintf("did:email:%x", hash)
vaultResp, err := dwnQueryClient.GetVaultByDID(context.Background(), &dwntypes.QueryGetVaultByDIDRequest{
Did: expectedDID,
})
suite.Require().NoError(err)
suite.Require().NotNil(vaultResp)
suite.Require().NotNil(vaultResp.Vault)
suite.Require().Equal(expectedDID, vaultResp.Vault.Did)
suite.Require().NotEmpty(vaultResp.Vault.PublicKey)
}
// TestAssertionUniqueness tests that duplicate assertions are rejected
func (suite *WebAuthnRegistrationTestSuite) TestAssertionUniqueness() {
val := suite.network.Validators[0]
email := "unique@example.com"
// First registration
msg1 := &didtypes.MsgRegisterWebAuthnCredential{
Creator: val.Address.String(),
Username: "user1",
AssertionValue: email,
AssertionType: "email",
WebauthnCredential: &didtypes.WebAuthnCredential{
CredentialId: "cred-1",
PublicKey: generateMockPublicKey(),
AttestationObject: generateMockAttestationObject(),
ClientDataJson: generateMockClientDataJSON(),
},
CreateVault: false,
}
clientCtx := val.ClientCtx
txResp1, err := broadcastTx(clientCtx, msg1)
suite.Require().NoError(err)
suite.Require().Equal(uint32(0), txResp1.Code)
// Wait for processing
time.Sleep(2 * time.Second)
// Attempt duplicate registration with same email
msg2 := &didtypes.MsgRegisterWebAuthnCredential{
Creator: val.Address.String(),
Username: "user2",
AssertionValue: email,
AssertionType: "email",
WebauthnCredential: &didtypes.WebAuthnCredential{
CredentialId: "cred-2",
PublicKey: generateMockPublicKey(),
AttestationObject: generateMockAttestationObject(),
ClientDataJson: generateMockClientDataJSON(),
},
CreateVault: false,
}
txResp2, err := broadcastTx(clientCtx, msg2)
// Should fail due to duplicate assertion
suite.Require().Error(err)
if txResp2 != nil {
suite.Require().NotEqual(uint32(0), txResp2.Code)
}
}
// TestUCANDelegationChain tests UCAN token creation during registration
func (suite *WebAuthnRegistrationTestSuite) TestUCANDelegationChain() {
val := suite.network.Validators[0]
username := "ucanuser"
email := "ucanuser@example.com"
msg := &didtypes.MsgRegisterWebAuthnCredential{
Creator: val.Address.String(),
Username: username,
AssertionValue: email,
AssertionType: "email",
WebauthnCredential: &didtypes.WebAuthnCredential{
CredentialId: "ucan-cred-id",
PublicKey: generateMockPublicKey(),
AttestationObject: generateMockAttestationObject(),
ClientDataJson: generateMockClientDataJSON(),
},
CreateVault: true,
}
clientCtx := val.ClientCtx
txResp, err := broadcastTx(clientCtx, msg)
suite.Require().NoError(err)
suite.Require().Equal(uint32(0), txResp.Code)
// Wait for processing
time.Sleep(2 * time.Second)
// Query DID document
queryClient := didtypes.NewQueryClient(clientCtx)
hasher := blake3.New()
hasher.Write([]byte(email))
hash := hasher.Sum(nil)
expectedDID := fmt.Sprintf("did:email:%x", hash)
didResp, err := queryClient.GetDIDDocument(context.Background(), &didtypes.QueryGetDIDDocumentRequest{
Did: expectedDID,
})
suite.Require().NoError(err)
suite.Require().NotNil(didResp.DidDocumentMetadata)
// Verify UCAN delegation chain reference exists in metadata
// In real implementation, this would check for the actual UCAN token
suite.Require().NotEmpty(didResp.DidDocumentMetadata.Created)
}
// TestMultipleAssertionTypes tests registration with both email and phone
func (suite *WebAuthnRegistrationTestSuite) TestMultipleAssertionTypes() {
val := suite.network.Validators[0]
testCases := []struct {
name string
username string
assertionType string
assertionValue string
}{
{
name: "register with email",
username: "emailuser",
assertionType: "email",
assertionValue: "multi@example.com",
},
{
name: "register with phone",
username: "phoneuser",
assertionType: "tel",
assertionValue: "+9876543210",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
msg := &didtypes.MsgRegisterWebAuthnCredential{
Creator: val.Address.String(),
Username: tc.username,
AssertionValue: tc.assertionValue,
AssertionType: tc.assertionType,
WebauthnCredential: &didtypes.WebAuthnCredential{
CredentialId: fmt.Sprintf("cred-%s", tc.username),
PublicKey: generateMockPublicKey(),
AttestationObject: generateMockAttestationObject(),
ClientDataJson: generateMockClientDataJSON(),
},
CreateVault: false,
}
clientCtx := val.ClientCtx
txResp, err := broadcastTx(clientCtx, msg)
suite.Require().NoError(err)
suite.Require().Equal(uint32(0), txResp.Code)
// Wait for processing
time.Sleep(2 * time.Second)
// Verify DID was created with correct prefix
hasher := blake3.New()
hasher.Write([]byte(tc.assertionValue))
hash := hasher.Sum(nil)
expectedDID := fmt.Sprintf("did:%s:%x", tc.assertionType, hash)
queryClient := didtypes.NewQueryClient(clientCtx)
didResp, err := queryClient.GetDIDDocument(context.Background(), &didtypes.QueryGetDIDDocumentRequest{
Did: expectedDID,
})
suite.Require().NoError(err)
suite.Require().NotNil(didResp.DidDocument)
suite.Require().Equal(expectedDID, didResp.DidDocument.Id)
})
}
}
// Helper functions
func broadcastTx(clientCtx client.Context, msg sdk.Msg) (*sdk.TxResponse, error) {
// This is a simplified version - in real tests, use proper tx broadcasting
// with proper fee calculation and signing
return nil, nil
}
func generateMockPublicKey() string {
// Generate a mock public key for testing
// In real tests, this would be a proper WebAuthn public key
return base64.StdEncoding.EncodeToString([]byte("mock-public-key"))
}
func generateMockAttestationObject() string {
// Generate a mock attestation object
attestation := map[string]interface{}{
"fmt": "packed",
"attStmt": map[string]interface{}{},
"authData": base64.StdEncoding.EncodeToString([]byte("mock-auth-data")),
}
data, _ := json.Marshal(attestation)
return base64.StdEncoding.EncodeToString(data)
}
func generateMockClientDataJSON() string {
// Generate mock client data
clientData := map[string]interface{}{
"type": "webauthn.create",
"challenge": "test-challenge",
"origin": "http://localhost:3000",
}
data, _ := json.Marshal(clientData)
return base64.StdEncoding.EncodeToString(data)
}
func TestWebAuthnRegistrationTestSuite(t *testing.T) {
suite.Run(t, new(WebAuthnRegistrationTestSuite))
}