* 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
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/make -f
DOCKER := $(shell which docker)
PROJECT_NAME = sonr-client-sdk
# golangci-lint Docker image version
GOLANGCI_VERSION := v2.3.1
###############################################################################
### Build ###
###############################################################################
build:
@gum log --level info "Building Go client SDK..."
@go build ./...
###############################################################################
### Formatting ###
###############################################################################
format fmt:
@gum log --level info "Formatting Go code with golangci-lint Docker..."
@$(DOCKER) run --rm -t -v $$(pwd):/app -w /app \
--user $$(id -u):$$(id -g) \
-v $$(go env GOCACHE):/.cache/go-build -e GOCACHE=/.cache/go-build \
-v $$(go env GOMODCACHE):/.cache/mod -e GOMODCACHE=/.cache/mod \
-v ~/.cache/golangci-lint:/.cache/golangci-lint -e GOLANGCI_LINT_CACHE=/.cache/golangci-lint \
golangci/golangci-lint:$(GOLANGCI_VERSION) golangci-lint run --fix --issues-exit-code=0
###############################################################################
### Linting ###
###############################################################################
lint:
@gum log --level info "Running golangci-lint Docker..."
@$(DOCKER) run --rm -t -v $$(pwd):/app -w /app \
--user $$(id -u):$$(id -g) \
-v $$(go env GOCACHE):/.cache/go-build -e GOCACHE=/.cache/go-build \
-v $$(go env GOMODCACHE):/.cache/mod -e GOMODCACHE=/.cache/mod \
-v ~/.cache/golangci-lint:/.cache/golangci-lint -e GOLANGCI_LINT_CACHE=/.cache/golangci-lint \
golangci/golangci-lint:$(GOLANGCI_VERSION) golangci-lint run --timeout=10m
###############################################################################
### Testing ###
###############################################################################
test:
@gum log --level info "Running all tests..."
@go test -mod=readonly -race -coverprofile=coverage.txt -covermode=atomic ./...
test-unit:
@gum log --level info "Running unit tests..."
@go test -mod=readonly -short -race ./...
test-integration:
@gum log --level info "Running integration tests..."
@go test -mod=readonly -race -tags=integration ./tests/integration/...
test-verbose:
@gum log --level info "Running tests with verbose output..."
@go test -mod=readonly -v -race ./...
test-cover:
@gum log --level info "Running tests with coverage..."
@go test -mod=readonly -race -coverprofile=coverage.txt -covermode=atomic ./...
@go tool cover -html=coverage.txt -o coverage.html
@gum log --level info "Coverage report generated: coverage.html"
benchmark:
@gum log --level info "Running benchmarks..."
@go test -mod=readonly -bench=. -benchmem ./...
###############################################################################
### Dependencies ###
###############################################################################
deps:
@gum log --level info "Installing dependencies..."
@go mod download
tidy:
@gum log --level info "Tidying dependencies..."
@go mod tidy
verify:
@gum log --level info "Verifying dependencies..."
@go mod verify
###############################################################################
### Clean ###
###############################################################################
clean:
@gum log --level info "Cleaning build artifacts..."
@rm -f coverage.txt coverage.html
@go clean -cache
###############################################################################
### Help ###
###############################################################################
help:
@gum log --level info "Available targets:"
@gum log --level info " build - Build the Go client SDK"
@gum log --level info " format/fmt - Format Go code using golangci-lint"
@gum log --level info " lint - Run golangci-lint"
@gum log --level info " test - Run all tests with coverage"
@gum log --level info " test-unit - Run unit tests only"
@gum log --level info " test-integration - Run integration tests"
@gum log --level info " test-verbose - Run tests with verbose output"
@gum log --level info " test-cover - Run tests and generate HTML coverage report"
@gum log --level info " benchmark - Run benchmarks"
@gum log --level info " deps - Download dependencies"
@gum log --level info " tidy - Tidy and verify dependencies"
@gum log --level info " verify - Verify dependencies"
@gum log --level info " clean - Clean build artifacts"
@gum log --level info " help - Show this help message"
.DEFAULT_GOAL := help
.PHONY: build format fmt lint test test-unit test-integration test-verbose test-cover benchmark deps tidy verify clean help
+169
View File
@@ -0,0 +1,169 @@
# Sonr Go Client SDK
The official Go client SDK for the Sonr blockchain, providing idiomatic Go interfaces for interacting with Sonr's decentralized identity, data storage, and service management features.
## Features
- **Blockchain Interaction**: Query chain state and broadcast transactions
- **Module Support**: Comprehensive support for DID, DWN, SVC, and UCAN modules
- **WebAuthn Integration**: Passwordless authentication with hardware-backed keys
- **Transaction Building**: Simplified transaction construction and broadcasting
- **Key Management**: Secure keyring integration with multiple backends
- **Network Configuration**: Pre-configured endpoints for testnet and mainnet
## Installation
```bash
go get github.com/sonr-io/sonr/client
```
## Quick Start
### Basic SDK Setup
```go
package main
import (
"context"
"log"
client "github.com/sonr-io/sonr/client"
)
func main() {
// Initialize SDK with testnet configuration
sdk, err := client.NewWithNetwork("testnet")
if err != nil {
log.Fatal(err)
}
defer sdk.Close()
// Or create with custom configuration
cfg := client.DefaultConfig()
cfg.Network.GRPCEndpoint = "localhost:9090"
sdk, err = client.New(cfg)
if err != nil {
log.Fatal(err)
}
// Check connection
if !sdk.IsConnected() {
log.Fatal("Failed to connect to network")
}
// Access the underlying Sonr client
sonrClient := sdk.Client()
// Query chain info
ctx := context.Background()
info, err := sonrClient.Query().GetNodeInfo(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("Connected to chain: %s", info.Network)
log.Printf("SDK Version: %s", client.Version())
}
```
### Transaction Broadcasting
```go
// Get the client from SDK
sonrClient := sdk.Client()
// Create and broadcast a transaction
tx := sonrClient.Transaction().
WithChainID("sonrtest_1-1").
WithGasPrice(0.001, "usnr").
WithMemo("Hello Sonr!")
// Add messages to transaction
tx.AddMessage(/* your message */)
// Sign and broadcast
result, err := tx.SignAndBroadcast(ctx, keyring)
if err != nil {
log.Fatal(err)
}
log.Printf("Transaction hash: %s", result.TxHash)
```
### Module-Specific Operations
```go
// Get the client from SDK
sonrClient := sdk.Client()
// DID operations
didClient := sonrClient.DID()
did, err := didClient.CreateDID(ctx, didOpts)
// DWN operations
dwnClient := sonrClient.DWN()
record, err := dwnClient.CreateRecord(ctx, recordData)
// Service operations
svcClient := sonrClient.SVC()
service, err := svcClient.RegisterService(ctx, serviceConfig)
// UCAN operations
ucanClient := sonrClient.UCAN()
capability, err := ucanClient.CreateCapability(ctx, capabilitySpec)
```
## API Overview
### Core Client (`sonr.Client`)
The main client provides access to:
- **Query operations**: Read blockchain state
- **Transaction building**: Create and broadcast transactions
- **Module clients**: Access to specialized functionality
- **Connection management**: Handle multiple endpoint types
### Module Clients
- **DID Client**: W3C DID operations with WebAuthn support
- **DWN Client**: Decentralized Web Node data management
- **SVC Client**: Service registration and management
- **UCAN Client**: User-Controlled Authorization Networks
### Key Management
- **Keyring integration**: Support for multiple keyring backends
- **Hardware keys**: WebAuthn and hardware wallet support
- **Multi-signature**: Threshold signature schemes
### Network Configuration
Pre-configured networks:
- **Testnet**: `sonrtest_1-1` with development endpoints
- **Mainnet**: Production network configuration (coming soon)
## Examples
See the `examples/` directory for complete working examples:
- **CLI Tool**: Example command-line application
- **Web Integration**: HTTP API server
- **Key Management**: Keyring and signing examples
- **Module Operations**: Comprehensive module usage
## Documentation
- [API Reference](https://pkg.go.dev/github.com/sonr-io/sonr/client)
- [Sonr Documentation](https://sonr.dev)
- [Blockchain Guide](https://sonr.dev/blockchain)
## Contributing
This SDK is part of the main Sonr repository. Please see the main project's contributing guidelines.
## License
This project is licensed under the Apache 2.0 License.
+363
View File
@@ -0,0 +1,363 @@
// Package auth provides gasless transaction support for WebAuthn operations.
package auth
import (
"context"
"encoding/base64"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/tx"
didtypes "github.com/sonr-io/sonr/x/did/types"
)
// GaslessTransactionManager handles gasless transactions for WebAuthn.
type GaslessTransactionManager interface {
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
CreateGaslessRegistration(ctx context.Context, credential *WebAuthnCredential, opts *GaslessRegistrationOptions) (*GaslessTransaction, error)
// BroadcastGasless broadcasts a gasless transaction.
BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error)
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
IsEligibleForGasless(msgs []sdk.Msg) bool
// EstimateGaslessGas estimates gas for a gasless transaction.
EstimateGaslessGas(msgType string) uint64
}
// GaslessRegistrationOptions configures gasless WebAuthn registration.
type GaslessRegistrationOptions struct {
Username string `json:"username"`
AutoCreateVault bool `json:"auto_create_vault"`
WebAuthnChallenge []byte `json:"webauthn_challenge"`
DIDDocument map[string]any `json:"did_document,omitempty"`
}
// GaslessTransaction represents a gasless transaction.
type GaslessTransaction struct {
Messages []sdk.Msg `json:"messages"`
Memo string `json:"memo"`
GasLimit uint64 `json:"gas_limit"`
SignerAddress string `json:"signer_address"`
SignMode signing.SignMode `json:"sign_mode"`
TxBytes []byte `json:"tx_bytes,omitempty"`
}
// BroadcastResult contains the result of broadcasting a transaction.
type BroadcastResult struct {
TxHash string `json:"tx_hash"`
Height int64 `json:"height"`
Code uint32 `json:"code"`
RawLog string `json:"raw_log"`
GasUsed int64 `json:"gas_used"`
GasWanted int64 `json:"gas_wanted"`
}
// gaslessManager implements GaslessTransactionManager.
type gaslessManager struct {
txBuilder tx.TxBuilder
broadcaster tx.Broadcaster
config *config.NetworkConfig
}
// NewGaslessTransactionManager creates a new gasless transaction manager.
func NewGaslessTransactionManager(
txBuilder tx.TxBuilder,
broadcaster tx.Broadcaster,
cfg *config.NetworkConfig,
) GaslessTransactionManager {
return &gaslessManager{
txBuilder: txBuilder,
broadcaster: broadcaster,
config: cfg,
}
}
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
func (gm *gaslessManager) CreateGaslessRegistration(
ctx context.Context,
credential *WebAuthnCredential,
opts *GaslessRegistrationOptions,
) (*GaslessTransaction, error) {
// Generate deterministic address from WebAuthn credential
signerAddr := gm.generateAddressFromWebAuthn(credential)
// Convert credential ID to base64 string
credentialID := base64.RawURLEncoding.EncodeToString(credential.RawID)
// Create WebAuthn credential for the message
webauthnCred := didtypes.WebAuthnCredential{
CredentialId: credentialID,
PublicKey: credential.PublicKey,
AttestationType: credential.AttestationType,
Origin: "http://localhost", // Default origin
Algorithm: -7, // ES256 algorithm
CreatedAt: time.Now().Unix(),
RpId: "localhost",
RpName: "Sonr Local",
Transports: credential.Transports,
UserVerified: false, // Default to false for gasless
}
// Set user verification if flags are available
if credential.Flags != nil {
webauthnCred.UserVerified = credential.Flags.UserVerified
}
// Create the registration message
msg := &didtypes.MsgRegisterWebAuthnCredential{
Controller: signerAddr.String(),
Username: opts.Username,
WebauthnCredential: webauthnCred,
AutoCreateVault: opts.AutoCreateVault,
}
// Create gasless transaction
gaslessTx := &GaslessTransaction{
Messages: []sdk.Msg{msg},
Memo: "WebAuthn Gasless Registration",
GasLimit: 200000, // Fixed gas limit for WebAuthn registration
SignerAddress: signerAddr.String(),
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
}
// Build the transaction using fluent interface
gm.txBuilder = gm.txBuilder.
ClearMessages().
AddMessage(msg).
WithMemo(gaslessTx.Memo).
WithGasLimit(gaslessTx.GasLimit).
WithFee(sdk.NewCoins()) // Zero fees for gasless
// Build unsigned transaction
unsignedTx, err := gm.txBuilder.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrInvalidTransaction, "failed to build gasless transaction")
}
// Store transaction bytes
gaslessTx.TxBytes = unsignedTx.SignBytes
return gaslessTx, nil
}
// BroadcastGasless broadcasts a gasless transaction.
func (gm *gaslessManager) BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error) {
// Broadcast the transaction using sync mode
resp, err := gm.broadcaster.BroadcastSync(ctx, tx.TxBytes)
if err != nil {
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast gasless transaction")
}
result := &BroadcastResult{
TxHash: resp.TxHash,
Height: resp.Height,
Code: resp.Code,
RawLog: resp.Log,
GasUsed: resp.GasUsed,
GasWanted: resp.GasWanted,
}
// Check for errors
if resp.Code != 0 {
return result, fmt.Errorf("transaction failed with code %d: %s", resp.Code, resp.Log)
}
return result, nil
}
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
func (gm *gaslessManager) IsEligibleForGasless(msgs []sdk.Msg) bool {
// Only single message transactions are eligible
if len(msgs) != 1 {
return false
}
// Check message type
msgType := sdk.MsgTypeURL(msgs[0])
// WebAuthn registration is gasless
if msgType == "/did.v1.MsgRegisterWebAuthnCredential" {
return true
}
// Future: Add other gasless message types here
return false
}
// EstimateGaslessGas estimates gas for a gasless transaction.
func (gm *gaslessManager) EstimateGaslessGas(msgType string) uint64 {
switch msgType {
case "/did.v1.MsgRegisterWebAuthnCredential":
return 200000 // Fixed gas for WebAuthn registration
default:
return 100000 // Default gas estimate
}
}
// generateAddressFromWebAuthn generates a deterministic address from WebAuthn credential.
func (gm *gaslessManager) generateAddressFromWebAuthn(credential *WebAuthnCredential) sdk.AccAddress {
// Use the credential ID as seed for address generation
// This ensures the same credential always generates the same address
// In production, this would use a proper derivation scheme
// For now, we'll use the first 20 bytes of the credential ID
addrBytes := make([]byte, 20)
copy(addrBytes, credential.RawID[:min(20, len(credential.RawID))])
return sdk.AccAddress(addrBytes)
}
// Helper function for min
func min(a, b int) int {
if a < b {
return a
}
return b
}
// WebAuthnGaslessClient provides a high-level interface for gasless WebAuthn operations.
type WebAuthnGaslessClient struct {
webauthnClient WebAuthnClient
gaslessManager GaslessTransactionManager
config *config.NetworkConfig
}
// NewWebAuthnGaslessClient creates a new WebAuthn gasless client.
func NewWebAuthnGaslessClient(
webauthnClient WebAuthnClient,
gaslessManager GaslessTransactionManager,
cfg *config.NetworkConfig,
) *WebAuthnGaslessClient {
return &WebAuthnGaslessClient{
webauthnClient: webauthnClient,
gaslessManager: gaslessManager,
config: cfg,
}
}
// RegisterGasless performs gasless WebAuthn registration.
func (wgc *WebAuthnGaslessClient) RegisterGasless(
ctx context.Context,
username string,
displayName string,
) (*GaslessRegistrationResult, error) {
// Create registration options
regOpts := &RegistrationOptions{
Username: username,
DisplayName: displayName,
Timeout: 60000,
UserVerification: "preferred",
AttestationType: "none",
}
// Begin WebAuthn registration
challenge, err := wgc.webauthnClient.BeginRegistration(ctx, regOpts)
if err != nil {
return nil, err
}
// Return challenge for browser to complete
// The actual credential will be created by the browser
result := &GaslessRegistrationResult{
Challenge: challenge,
GaslessEligible: true,
EstimatedGas: wgc.gaslessManager.EstimateGaslessGas("/did.v1.MsgRegisterWebAuthnCredential"),
}
return result, nil
}
// CompleteGaslessRegistration completes the gasless registration after browser response.
func (wgc *WebAuthnGaslessClient) CompleteGaslessRegistration(
ctx context.Context,
challenge *RegistrationChallenge,
response *AuthenticatorAttestationResponse,
username string,
autoCreateVault bool,
) (*BroadcastResult, error) {
// Complete WebAuthn registration to get credential
credential, err := wgc.webauthnClient.CompleteRegistration(ctx, challenge, response)
if err != nil {
// For now, create a mock credential since CompleteRegistration is not fully implemented
// In production, this would properly parse the attestation response
credential = &WebAuthnCredential{
ID: base64.URLEncoding.EncodeToString(response.AttestationObject[:32]),
RawID: response.AttestationObject[:32],
PublicKey: response.AttestationObject[32:64], // Mock public key
AttestationType: "none",
Flags: &AuthenticatorFlags{
UserPresent: true,
UserVerified: true,
},
Authenticator: &AuthenticatorData{
RPIDHash: challenge.Challenge,
},
UserID: string(challenge.User.ID),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
}
// Create gasless registration options
gaslessOpts := &GaslessRegistrationOptions{
Username: username,
AutoCreateVault: autoCreateVault,
WebAuthnChallenge: challenge.Challenge,
}
// Create gasless transaction
gaslessTx, err := wgc.gaslessManager.CreateGaslessRegistration(ctx, credential, gaslessOpts)
if err != nil {
return nil, err
}
// Broadcast gasless transaction
return wgc.gaslessManager.BroadcastGasless(ctx, gaslessTx)
}
// GaslessRegistrationResult contains the result of initiating gasless registration.
type GaslessRegistrationResult struct {
Challenge *RegistrationChallenge `json:"challenge"`
GaslessEligible bool `json:"gasless_eligible"`
EstimatedGas uint64 `json:"estimated_gas"`
}
// ValidateGaslessEligibility validates if a user is eligible for gasless transactions.
func ValidateGaslessEligibility(credential *WebAuthnCredential) error {
// Validate credential is not nil
if credential == nil {
return fmt.Errorf("credential cannot be nil")
}
// Validate credential has required fields
if len(credential.RawID) == 0 {
return fmt.Errorf("credential ID cannot be empty")
}
if len(credential.PublicKey) == 0 {
return fmt.Errorf("public key cannot be empty")
}
// Validate user presence and verification
if credential.Flags != nil {
if !credential.Flags.UserPresent {
return fmt.Errorf("user presence is required for gasless transactions")
}
}
return nil
}
// GetGaslessEndpoint returns the gasless transaction endpoint for the network.
func GetGaslessEndpoint(cfg *config.NetworkConfig) string {
// For now, gasless transactions use the same RPC endpoint
// In the future, this could be a separate endpoint
return cfg.RPC
}
+463
View File
@@ -0,0 +1,463 @@
package auth
import (
"context"
"encoding/base64"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/keys"
"github.com/sonr-io/sonr/client/tx"
didtypes "github.com/sonr-io/sonr/x/did/types"
)
// MockTxBuilder implements tx.TxBuilder for testing.
type MockTxBuilder struct {
messages []sdk.Msg
config *tx.TxConfig
}
func NewMockTxBuilder() *MockTxBuilder {
return &MockTxBuilder{
messages: make([]sdk.Msg, 0),
config: &tx.TxConfig{
ChainID: "test-chain",
GasPrice: 0.001,
GasDenom: "usnr",
GasLimit: 200000,
GasAdjustment: 1.5,
},
}
}
func (m *MockTxBuilder) WithChainID(chainID string) tx.TxBuilder {
m.config.ChainID = chainID
return m
}
func (m *MockTxBuilder) WithGasPrice(price float64, denom string) tx.TxBuilder {
m.config.GasPrice = price
m.config.GasDenom = denom
return m
}
func (m *MockTxBuilder) WithGasLimit(limit uint64) tx.TxBuilder {
m.config.GasLimit = limit
return m
}
func (m *MockTxBuilder) WithMemo(memo string) tx.TxBuilder {
m.config.Memo = memo
return m
}
func (m *MockTxBuilder) WithTimeoutHeight(height uint64) tx.TxBuilder {
m.config.TimeoutHeight = height
return m
}
func (m *MockTxBuilder) AddMessage(msg sdk.Msg) tx.TxBuilder {
m.messages = append(m.messages, msg)
return m
}
func (m *MockTxBuilder) AddMessages(msgs ...sdk.Msg) tx.TxBuilder {
m.messages = append(m.messages, msgs...)
return m
}
func (m *MockTxBuilder) ClearMessages() tx.TxBuilder {
m.messages = make([]sdk.Msg, 0)
return m
}
func (m *MockTxBuilder) WithFee(amount sdk.Coins) tx.TxBuilder {
m.config.Fee = amount
return m
}
func (m *MockTxBuilder) WithGasAdjustment(adjustment float64) tx.TxBuilder {
m.config.GasAdjustment = adjustment
return m
}
func (m *MockTxBuilder) EstimateGas(ctx context.Context) (uint64, error) {
return 200000, nil
}
func (m *MockTxBuilder) Sign(ctx context.Context, keyring keys.KeyringManager) (*tx.SignedTx, error) {
unsignedTx, _ := m.Build()
return &tx.SignedTx{
UnsignedTx: unsignedTx,
Signature: []byte("mock-signature"),
PubKey: []byte("mock-pubkey"),
TxBytes: []byte("mock-tx-bytes"),
}, nil
}
func (m *MockTxBuilder) SignAndBroadcast(ctx context.Context, keyring keys.KeyringManager) (*tx.BroadcastResult, error) {
return &tx.BroadcastResult{
TxHash: "mock-tx-hash",
Height: 12345,
Code: 0,
Log: "success",
GasUsed: 100000,
GasWanted: 200000,
}, nil
}
func (m *MockTxBuilder) Broadcast(ctx context.Context, signedTx *tx.SignedTx) (*tx.BroadcastResult, error) {
return &tx.BroadcastResult{
TxHash: "mock-tx-hash",
Height: 12345,
Code: 0,
Log: "success",
GasUsed: 100000,
GasWanted: 200000,
}, nil
}
func (m *MockTxBuilder) Simulate(ctx context.Context) (*tx.SimulateResult, error) {
return &tx.SimulateResult{
GasWanted: 200000,
GasUsed: 100000,
Log: "simulation success",
}, nil
}
func (m *MockTxBuilder) Build() (*tx.UnsignedTx, error) {
return &tx.UnsignedTx{
Messages: m.messages,
Config: m.config,
SignBytes: []byte("mock-sign-bytes"),
}, nil
}
func (m *MockTxBuilder) BuildSigned(signature []byte, pubKey []byte) (*tx.SignedTx, error) {
unsignedTx, _ := m.Build()
return &tx.SignedTx{
UnsignedTx: unsignedTx,
Signature: signature,
PubKey: pubKey,
TxBytes: append(unsignedTx.SignBytes, signature...),
}, nil
}
func (m *MockTxBuilder) Config() *tx.TxConfig {
return m.config
}
// MockBroadcaster implements tx.Broadcaster for testing.
type MockBroadcaster struct {
broadcastedTxs [][]byte
shouldFail bool
}
func NewMockBroadcaster() *MockBroadcaster {
return &MockBroadcaster{
broadcastedTxs: make([][]byte, 0),
shouldFail: false,
}
}
func (m *MockBroadcaster) Broadcast(ctx context.Context, txBytes []byte, mode tx.BroadcastMode) (*tx.BroadcastResult, error) {
m.broadcastedTxs = append(m.broadcastedTxs, txBytes)
if m.shouldFail {
return &tx.BroadcastResult{
Code: 1,
Log: "mock error",
}, nil
}
return &tx.BroadcastResult{
TxHash: "mock-tx-hash",
Height: 12345,
Code: 0,
Log: "success",
GasUsed: 100000,
GasWanted: 200000,
}, nil
}
func (m *MockBroadcaster) BroadcastSync(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, tx.BroadcastModeSync)
}
func (m *MockBroadcaster) BroadcastAsync(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, tx.BroadcastModeAsync)
}
func (m *MockBroadcaster) BroadcastBlock(ctx context.Context, txBytes []byte) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, tx.BroadcastModeBlock)
}
func (m *MockBroadcaster) BroadcastWithRetry(ctx context.Context, txBytes []byte, mode tx.BroadcastMode, maxRetries int) (*tx.BroadcastResult, error) {
return m.Broadcast(ctx, txBytes, mode)
}
func (m *MockBroadcaster) WaitForConfirmation(ctx context.Context, txHash string, timeout time.Duration) (*tx.TxConfirmation, error) {
return &tx.TxConfirmation{
TxHash: txHash,
BlockHeight: 12345,
BlockTime: time.Now(),
Code: 0,
Log: "confirmed",
GasWanted: 200000,
GasUsed: 100000,
}, nil
}
func (m *MockBroadcaster) WithRetryConfig(config tx.RetryConfig) tx.Broadcaster {
return m
}
func (m *MockBroadcaster) WithTimeout(timeout time.Duration) tx.Broadcaster {
return m
}
// GaslessTestSuite tests gasless transaction functionality.
type GaslessTestSuite struct {
suite.Suite
manager GaslessTransactionManager
txBuilder tx.TxBuilder
broadcaster tx.Broadcaster
config *config.NetworkConfig
}
func (suite *GaslessTestSuite) SetupTest() {
cfg := config.LocalNetwork()
suite.config = &cfg
suite.txBuilder = NewMockTxBuilder()
suite.broadcaster = NewMockBroadcaster()
suite.manager = NewGaslessTransactionManager(
suite.txBuilder,
suite.broadcaster,
suite.config,
)
}
func (suite *GaslessTestSuite) TestCreateGaslessRegistration() {
// Create test WebAuthn credential
credential := &WebAuthnCredential{
ID: "test-credential-id",
RawID: []byte("test-raw-id"),
PublicKey: []byte("test-public-key"),
AttestationType: "none",
Transports: []string{"usb"},
Flags: &AuthenticatorFlags{
UserPresent: true,
UserVerified: true,
},
Authenticator: &AuthenticatorData{
RPIDHash: []byte("test-rp-id-hash"),
},
}
// Create options
opts := &GaslessRegistrationOptions{
Username: "testuser",
AutoCreateVault: true,
WebAuthnChallenge: []byte("test-challenge"),
}
// Create gasless registration
gaslessTx, err := suite.manager.CreateGaslessRegistration(context.Background(), credential, opts)
suite.Require().NoError(err)
suite.Require().NotNil(gaslessTx)
// Verify transaction fields
suite.Equal("WebAuthn Gasless Registration", gaslessTx.Memo)
suite.Equal(uint64(200000), gaslessTx.GasLimit)
suite.Equal(signing.SignMode_SIGN_MODE_DIRECT, gaslessTx.SignMode)
suite.Len(gaslessTx.Messages, 1)
// Verify message type
msg, ok := gaslessTx.Messages[0].(*didtypes.MsgRegisterWebAuthnCredential)
suite.Require().True(ok)
suite.Equal("testuser", msg.Username)
suite.True(msg.AutoCreateVault)
}
func (suite *GaslessTestSuite) TestBroadcastGasless() {
// Create test transaction
gaslessTx := &GaslessTransaction{
Messages: []sdk.Msg{
&didtypes.MsgRegisterWebAuthnCredential{
Controller: "sonr1xyz...",
Username: "testuser",
},
},
Memo: "Test Gasless",
GasLimit: 200000,
SignerAddress: "sonr1xyz...",
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
TxBytes: []byte("test-tx-bytes"),
}
// Broadcast transaction
result, err := suite.manager.BroadcastGasless(context.Background(), gaslessTx)
suite.Require().NoError(err)
suite.Require().NotNil(result)
// Verify result
suite.Equal("mock-tx-hash", result.TxHash)
suite.Equal(int64(12345), result.Height)
suite.Equal(uint32(0), result.Code)
}
func (suite *GaslessTestSuite) TestIsEligibleForGasless() {
// Test WebAuthn registration message
webauthnMsg := &didtypes.MsgRegisterWebAuthnCredential{
Controller: "sonr1xyz...",
Username: "testuser",
}
// Should be eligible
suite.True(suite.manager.IsEligibleForGasless([]sdk.Msg{webauthnMsg}))
// Multiple messages should not be eligible
suite.False(suite.manager.IsEligibleForGasless([]sdk.Msg{webauthnMsg, webauthnMsg}))
// Other message types should not be eligible
otherMsg := &didtypes.MsgCreateDID{
Controller: "sonr1xyz...",
}
suite.False(suite.manager.IsEligibleForGasless([]sdk.Msg{otherMsg}))
}
func (suite *GaslessTestSuite) TestEstimateGaslessGas() {
// Test WebAuthn registration gas estimate
gas := suite.manager.EstimateGaslessGas("/did.v1.MsgRegisterWebAuthnCredential")
suite.Equal(uint64(200000), gas)
// Test default gas estimate
gas = suite.manager.EstimateGaslessGas("/unknown.message.type")
suite.Equal(uint64(100000), gas)
}
func TestGaslessTestSuite(t *testing.T) {
suite.Run(t, new(GaslessTestSuite))
}
// TestValidateGaslessEligibility tests credential validation.
func TestValidateGaslessEligibility(t *testing.T) {
tests := []struct {
name string
credential *WebAuthnCredential
wantError bool
}{
{
name: "valid credential",
credential: &WebAuthnCredential{
RawID: []byte("test-id"),
PublicKey: []byte("test-key"),
Flags: &AuthenticatorFlags{
UserPresent: true,
},
},
wantError: false,
},
{
name: "nil credential",
credential: nil,
wantError: true,
},
{
name: "empty credential ID",
credential: &WebAuthnCredential{
PublicKey: []byte("test-key"),
Flags: &AuthenticatorFlags{
UserPresent: true,
},
},
wantError: true,
},
{
name: "empty public key",
credential: &WebAuthnCredential{
RawID: []byte("test-id"),
Flags: &AuthenticatorFlags{
UserPresent: true,
},
},
wantError: true,
},
{
name: "no user presence",
credential: &WebAuthnCredential{
RawID: []byte("test-id"),
PublicKey: []byte("test-key"),
Flags: &AuthenticatorFlags{
UserPresent: false,
},
},
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateGaslessEligibility(tt.credential)
if tt.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// TestGetGaslessEndpoint tests endpoint retrieval.
func TestGetGaslessEndpoint(t *testing.T) {
cfg := &config.NetworkConfig{
RPC: "http://localhost:26657",
}
endpoint := GetGaslessEndpoint(cfg)
require.Equal(t, "http://localhost:26657", endpoint)
}
// TestWebAuthnCredentialConversion tests conversion between credential types.
func TestWebAuthnCredentialConversion(t *testing.T) {
// Create client credential
clientCred := &WebAuthnCredential{
ID: base64.RawURLEncoding.EncodeToString([]byte("test-id")),
RawID: []byte("test-id"),
PublicKey: []byte("test-public-key"),
AttestationType: "none",
Transports: []string{"usb", "nfc"},
Flags: &AuthenticatorFlags{
UserPresent: true,
UserVerified: false,
},
}
// Convert to protobuf credential
protoCred := didtypes.WebAuthnCredential{
CredentialId: base64.RawURLEncoding.EncodeToString(clientCred.RawID),
PublicKey: clientCred.PublicKey,
AttestationType: clientCred.AttestationType,
Origin: "http://localhost",
Algorithm: -7, // ES256
CreatedAt: time.Now().Unix(),
RpId: "localhost",
RpName: "Sonr Local",
Transports: clientCred.Transports,
UserVerified: clientCred.Flags.UserVerified,
}
// Verify conversion
require.Equal(t, base64.RawURLEncoding.EncodeToString([]byte("test-id")), protoCred.CredentialId)
require.Equal(t, clientCred.PublicKey, protoCred.PublicKey)
require.Equal(t, clientCred.AttestationType, protoCred.AttestationType)
require.Equal(t, clientCred.Transports, protoCred.Transports)
require.Equal(t, clientCred.Flags.UserVerified, protoCred.UserVerified)
}
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
package auth
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWebAuthnClient_BasicRegistration(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
rpName: "Sonr",
}
// Test BeginRegistration
opts := &RegistrationOptions{
UserID: "test_user",
Username: "testuser",
DisplayName: "Test User",
}
challenge, err := client.BeginRegistration(context.Background(), opts)
require.NoError(t, err)
assert.NotNil(t, challenge)
assert.NotEmpty(t, challenge.Challenge)
assert.Equal(t, "localhost", challenge.RelyingParty.ID)
assert.Equal(t, "Sonr", challenge.RelyingParty.Name)
assert.Equal(t, []byte("test_user"), challenge.User.ID)
assert.Equal(t, "testuser", challenge.User.Name)
assert.Equal(t, "Test User", challenge.User.DisplayName)
}
func TestWebAuthnClient_BasicAuthentication(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
}
// Test BeginAuthentication
opts := &AuthenticationOptions{
UserVerification: "preferred",
Timeout: 60000,
}
challenge, err := client.BeginAuthentication(context.Background(), opts)
require.NoError(t, err)
assert.NotNil(t, challenge)
assert.NotEmpty(t, challenge.Challenge)
assert.Equal(t, "localhost", challenge.RelyingPartyID)
assert.Equal(t, "preferred", challenge.UserVerification)
assert.Equal(t, 60000, challenge.Timeout)
}
func TestWebAuthnClient_VerifyClientData(t *testing.T) {
challenge := []byte("test_challenge")
origin := "http://localhost"
// Create valid client data
clientData := map[string]any{
"type": "webauthn.create",
"challenge": "dGVzdF9jaGFsbGVuZ2U", // base64url encoded "test_challenge"
"origin": origin,
}
clientDataJSON, _ := json.Marshal(clientData)
// Test successful verification
result, err := verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "webauthn.create", string(result.Type))
assert.Equal(t, origin, result.Origin)
// Test wrong type
clientData["type"] = "webauthn.get"
clientDataJSON, _ = json.Marshal(clientData)
_, err = verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
assert.Error(t, err)
// Test wrong origin
clientData["type"] = "webauthn.create"
clientData["origin"] = "http://evil.com"
clientDataJSON, _ = json.Marshal(clientData)
_, err = verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
assert.Error(t, err)
// Test wrong challenge
clientData["origin"] = origin
clientData["challenge"] = "d3JvbmdfY2hhbGxlbmdl" // base64url encoded "wrong_challenge"
clientDataJSON, _ = json.Marshal(clientData)
_, err = verifyClientData(clientDataJSON, challenge, "webauthn.create", origin)
assert.Error(t, err)
}
+338
View File
@@ -0,0 +1,338 @@
package auth
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/x/did/types"
)
// Mock DIDClient for testing
type mockDIDClient struct {
credentials []*types.WebAuthnCredential
didDoc *types.DIDDocument
error error
}
func (m *mockDIDClient) QueryCredentials(ctx context.Context, did string) ([]*types.WebAuthnCredential, error) {
if m.error != nil {
return nil, m.error
}
return m.credentials, nil
}
func (m *mockDIDClient) QueryCredential(ctx context.Context, did, credentialID string) (*types.WebAuthnCredential, error) {
if m.error != nil {
return nil, m.error
}
for _, cred := range m.credentials {
if cred.CredentialId == credentialID {
return cred, nil
}
}
return nil, fmt.Errorf("key not found")
}
func (m *mockDIDClient) UpdateCredential(ctx context.Context, did string, credential *types.WebAuthnCredential) error {
if m.error != nil {
return m.error
}
for i, cred := range m.credentials {
if cred.CredentialId == credential.CredentialId {
m.credentials[i] = credential
return nil
}
}
return fmt.Errorf("key not found")
}
func (m *mockDIDClient) RevokeCredential(ctx context.Context, did, credentialID string) error {
if m.error != nil {
return m.error
}
for i, cred := range m.credentials {
if cred.CredentialId == credentialID {
// Remove from slice
m.credentials = append(m.credentials[:i], m.credentials[i+1:]...)
return nil
}
}
return fmt.Errorf("key not found")
}
func (m *mockDIDClient) QueryDIDDocument(ctx context.Context, did string) (*types.DIDDocument, error) {
if m.error != nil {
return nil, m.error
}
return m.didDoc, nil
}
func (m *mockDIDClient) AuthenticateWithDID(ctx context.Context, did string, assertion []byte) (bool, error) {
if m.error != nil {
return false, m.error
}
return true, nil
}
func (m *mockDIDClient) SignTransaction(ctx context.Context, did string, txBytes []byte, credentialID string) ([]byte, error) {
if m.error != nil {
return nil, m.error
}
// Mock signature
return []byte("mock_signature"), nil
}
// Helper function to create test credentials
func createTestCredential(id string) *types.WebAuthnCredential {
return &types.WebAuthnCredential{
CredentialId: id,
RawId: base64.RawURLEncoding.EncodeToString([]byte(id)),
ClientDataJson: `{"type":"webauthn.create","challenge":"test","origin":"http://localhost"}`,
AttestationObject: base64.RawURLEncoding.EncodeToString([]byte("test_attestation")),
PublicKey: []byte("test_public_key"),
Algorithm: -7, // ES256
Origin: "http://localhost",
CreatedAt: 1234567890,
}
}
func TestWebAuthnClient_CompleteRegistration(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
}
challenge := &RegistrationChallenge{
Challenge: []byte("test_challenge"),
User: &User{
ID: []byte("test_user"),
Name: "Test User",
DisplayName: "Test",
},
}
// Create valid client data JSON
clientData := map[string]any{
"type": "webauthn.create",
"challenge": challenge.Challenge,
"origin": "http://localhost",
}
clientDataJSON, _ := json.Marshal(clientData)
response := &AuthenticatorAttestationResponse{
ClientDataJSON: clientDataJSON,
AttestationObject: []byte("test_attestation"),
}
// Test successful registration
cred, err := client.CompleteRegistration(context.Background(), challenge, response)
assert.NoError(t, err)
assert.NotNil(t, cred)
// Test with invalid client data
response.ClientDataJSON = []byte("invalid_json")
_, err = client.CompleteRegistration(context.Background(), challenge, response)
assert.Error(t, err)
}
func TestWebAuthnClient_CompleteAuthentication(t *testing.T) {
credential := createTestCredential("test_cred_1")
client := &webAuthnClient{
origin: "http://localhost",
rpID: "localhost",
}
challenge := &AuthenticationChallenge{
Challenge: []byte("test_challenge"),
}
// Create valid client data JSON
clientData := map[string]any{
"type": "webauthn.get",
"challenge": challenge.Challenge,
"origin": "http://localhost",
}
clientDataJSON, _ := json.Marshal(clientData)
response := &AuthenticatorAssertionResponse{
ClientDataJSON: clientDataJSON,
AuthenticatorData: []byte("test_auth_data"),
Signature: []byte("test_signature"),
UserHandle: []byte("test_user"),
}
// Test successful authentication
result, err := client.CompleteAuthentication(context.Background(), challenge, response, "test_cred_1")
assert.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Verified)
// Test with wrong origin
clientData["origin"] = "http://evil.com"
clientDataJSON, _ = json.Marshal(clientData)
response.ClientDataJSON = base64.RawURLEncoding.EncodeToString(clientDataJSON)
_, err = client.CompleteAuthentication(context.Background(), challenge, response, "test_cred_1")
assert.Error(t, err)
}
func TestWebAuthnClient_ListCredentials(t *testing.T) {
cred1 := createTestCredential("cred1")
cred2 := createTestCredential("cred2")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred1, cred2},
},
}
// Test successful list
creds, err := client.ListCredentials(context.Background(), "did:test:123")
require.NoError(t, err)
assert.Len(t, creds, 2)
assert.Equal(t, "cred1", creds[0].CredentialId)
assert.Equal(t, "cred2", creds[1].CredentialId)
// Test with error
client.didClient = &mockDIDClient{error: assert.AnError}
_, err = client.ListCredentials(context.Background(), "did:test:123")
assert.Error(t, err)
}
func TestWebAuthnClient_GetCredential(t *testing.T) {
cred := createTestCredential("test_cred")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred},
},
}
// Test successful get
retrieved, err := client.GetCredential(context.Background(), "did:test:123", "test_cred")
require.NoError(t, err)
assert.Equal(t, cred.CredentialId, retrieved.CredentialId)
// Test credential not found
_, err = client.GetCredential(context.Background(), "did:test:123", "non_existent")
assert.Error(t, err)
}
func TestWebAuthnClient_UpdateCredential(t *testing.T) {
cred := createTestCredential("test_cred")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred},
},
}
// Update the credential
updatedCred := createTestCredential("test_cred")
updatedCred.Origin = "https://updated.example.com"
err := client.UpdateCredential(context.Background(), "did:test:123", updatedCred)
require.NoError(t, err)
// Verify update
mock := client.didClient.(*mockDIDClient)
assert.Equal(t, "https://updated.example.com", mock.credentials[0].Origin)
// Test update non-existent credential
nonExistent := createTestCredential("non_existent")
err = client.UpdateCredential(context.Background(), "did:test:123", nonExistent)
assert.Error(t, err)
}
func TestWebAuthnClient_RevokeCredential(t *testing.T) {
cred1 := createTestCredential("cred1")
cred2 := createTestCredential("cred2")
client := &webAuthnClient{
didClient: &mockDIDClient{
credentials: []*types.WebAuthnCredential{cred1, cred2},
},
}
// Revoke first credential
err := client.RevokeCredential(context.Background(), "did:test:123", "cred1")
require.NoError(t, err)
// Verify revocation
mock := client.didClient.(*mockDIDClient)
assert.Len(t, mock.credentials, 1)
assert.Equal(t, "cred2", mock.credentials[0].CredentialId)
// Test revoke non-existent credential
err = client.RevokeCredential(context.Background(), "did:test:123", "non_existent")
assert.Error(t, err)
}
func TestWebAuthnClient_AuthenticateWithDID(t *testing.T) {
client := &webAuthnClient{
origin: "http://localhost",
didClient: &mockDIDClient{
didDoc: &types.DIDDocument{
Id: "did:test:123",
},
},
}
assertion := &AuthenticationAssertion{
CredentialID: "test_cred",
ClientDataJSON: base64.RawURLEncoding.EncodeToString([]byte(`{"type":"webauthn.get"}`)),
AuthenticatorData: base64.RawURLEncoding.EncodeToString([]byte("auth_data")),
Signature: base64.RawURLEncoding.EncodeToString([]byte("signature")),
UserHandle: base64.RawURLEncoding.EncodeToString([]byte("user")),
}
// Test successful authentication
result, err := client.AuthenticateWithDID(context.Background(), "did:test:123", assertion)
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Success)
assert.Equal(t, "test_cred", result.CredentialId)
// Test with error
client.didClient = &mockDIDClient{error: assert.AnError}
_, err = client.AuthenticateWithDID(context.Background(), "did:test:123", assertion)
assert.Error(t, err)
}
func TestWebAuthnClient_SignWithWebAuthn(t *testing.T) {
client := &webAuthnClient{
didClient: &mockDIDClient{},
}
txData := []byte("transaction_data")
// Test successful signing
sig, err := client.SignWithWebAuthn(context.Background(), "did:test:123", txData, "test_cred")
require.NoError(t, err)
assert.Equal(t, []byte("mock_signature"), sig)
// Test with error
client.didClient = &mockDIDClient{error: assert.AnError}
_, err = client.SignWithWebAuthn(context.Background(), "did:test:123", txData, "test_cred")
assert.Error(t, err)
}
func TestWebAuthnClient_BindCredential(t *testing.T) {
client := &webAuthnClient{}
cred := createTestCredential("test_cred")
// Test that BindCredential returns the existing implementation message
result, err := client.BindCredential(context.Background(), "did:test:123", cred)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "test_cred", result.CredentialId)
// The actual binding logic would be implemented in a later phase
// For now, it just returns the credential as-is
}
+181
View File
@@ -0,0 +1,181 @@
// Package client provides a high-level interface for interacting with the Sonr blockchain.
package client
import (
"context"
"fmt"
"time"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/sonr"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// SDK represents the main entry point for the Sonr Go Client SDK
type SDK struct {
client sonr.Client
config *Config
grpcConn *grpc.ClientConn
}
// Config holds SDK configuration
type Config struct {
// Network configuration
Network *config.NetworkConfig
// Connection timeout
Timeout time.Duration
// Enable debug logging
Debug bool
// Custom gRPC dial options
GRPCOptions []grpc.DialOption
}
// DefaultConfig returns default SDK configuration for testnet
func DefaultConfig() *Config {
clientCfg := config.TestnetConfig()
return &Config{
Network: &clientCfg.Network,
Timeout: 30 * time.Second,
Debug: false,
GRPCOptions: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
},
}
}
// LocalConfig returns SDK configuration for local development
func LocalConfig() *Config {
clientCfg := config.LocalConfig()
return &Config{
Network: &clientCfg.Network,
Timeout: 30 * time.Second,
Debug: true,
GRPCOptions: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
},
}
}
// New creates a new SDK instance with the given configuration
func New(cfg *Config) (*SDK, error) {
if cfg == nil {
cfg = DefaultConfig()
}
if cfg.Network == nil {
return nil, fmt.Errorf("network configuration is required")
}
// Create gRPC connection
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
grpcConn, err := grpc.DialContext(
ctx,
cfg.Network.GRPC,
cfg.GRPCOptions...,
)
if err != nil {
return nil, fmt.Errorf("failed to connect to gRPC endpoint: %w", err)
}
// Create the main client
clientCfg := &config.ClientConfig{
Network: *cfg.Network,
KeyringBackend: "test", // Default to test for now
}
client, err := sonr.NewClient(clientCfg)
if err != nil {
grpcConn.Close()
return nil, fmt.Errorf("failed to create client: %w", err)
}
return &SDK{
client: client,
config: cfg,
grpcConn: grpcConn,
}, nil
}
// NewWithNetwork creates a new SDK instance for a specific network
func NewWithNetwork(network string) (*SDK, error) {
var cfg *Config
switch network {
case "testnet":
cfg = DefaultConfig()
case "local":
cfg = LocalConfig()
case "localapi":
clientCfg := config.LocalAPIConfig()
cfg = &Config{
Network: &clientCfg.Network,
Timeout: 30 * time.Second,
Debug: true,
GRPCOptions: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
},
}
default:
return nil, fmt.Errorf("unknown network: %s (supported: testnet, local, localapi)", network)
}
return New(cfg)
}
// Client returns the underlying Sonr client
func (s *SDK) Client() sonr.Client {
return s.client
}
// Config returns the SDK configuration
func (s *SDK) Config() *Config {
return s.config
}
// Close closes all connections
func (s *SDK) Close() error {
if s.grpcConn != nil {
return s.grpcConn.Close()
}
return nil
}
// WithTimeout returns a new SDK instance with updated timeout
func (s *SDK) WithTimeout(timeout time.Duration) *SDK {
s.config.Timeout = timeout
if s.client != nil {
// Update client config timeout
clientCfg := &config.ClientConfig{
Network: *s.config.Network,
KeyringBackend: "test",
}
client, _ := sonr.NewClient(clientCfg)
s.client = client
}
return s
}
// IsConnected checks if the SDK is connected to the network
func (s *SDK) IsConnected() bool {
if s.grpcConn == nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Try to get node info to check connection
_, err := s.client.Query().NodeInfo(ctx)
return err == nil
}
// Version returns the SDK version
func Version() string {
return "v0.1.0"
}
+344
View File
@@ -0,0 +1,344 @@
// Package config provides network configuration and connection settings for the Sonr client SDK.
package config
import (
"errors"
"fmt"
"time"
)
// NetworkConfig defines the configuration for connecting to a Sonr network.
type NetworkConfig struct {
// Network identification
ChainID string `json:"chain_id"`
Name string `json:"name"`
NetworkID string `json:"network_id"`
// Endpoints for different connection types
GRPC string `json:"grpc_endpoint"`
REST string `json:"rest_endpoint"`
RPC string `json:"rpc_endpoint"`
// Token configuration
Denom string `json:"denom"` // Normal denomination (snr)
StakingDenom string `json:"staking_denom"` // Staking denomination (usnr)
// Gas configuration
GasPrice float64 `json:"gas_price"` // Default gas price
GasAdjustment float64 `json:"gas_adjustment"` // Gas adjustment factor
// Connection settings
RequestTimeout time.Duration `json:"request_timeout"`
MaxRetries int `json:"max_retries"`
RetryDelay time.Duration `json:"retry_delay"`
// TLS configuration
Insecure bool `json:"insecure"` // Disable TLS verification (for development)
// Additional endpoints for specialized services
IPFSGateway string `json:"ipfs_gateway,omitempty"`
HighwayAPI string `json:"highway_api,omitempty"`
}
// ClientConfig defines the overall configuration for the Sonr client.
type ClientConfig struct {
// Network configuration
Network NetworkConfig `json:"network"`
// Keyring configuration
KeyringBackend string `json:"keyring_backend,omitempty"` // os, file, test, memory
KeyringDir string `json:"keyring_dir,omitempty"` // Directory for file backend
// Logging configuration
LogLevel string `json:"log_level,omitempty"` // debug, info, warn, error
LogFormat string `json:"log_format,omitempty"` // json, text
// Transaction configuration
BroadcastMode string `json:"broadcast_mode,omitempty"` // sync, async, block
// Feature flags
EnableMetrics bool `json:"enable_metrics,omitempty"`
EnableTracing bool `json:"enable_tracing,omitempty"`
}
// DefaultConfig returns a default client configuration using the testnet.
func DefaultConfig() *ClientConfig {
return &ClientConfig{
Network: TestnetNetwork(),
KeyringBackend: "test",
LogLevel: "info",
LogFormat: "text",
BroadcastMode: "sync",
EnableMetrics: false,
EnableTracing: false,
}
}
// TestnetConfig returns a client configuration for the Sonr testnet.
func TestnetConfig() *ClientConfig {
config := DefaultConfig()
config.Network = TestnetNetwork()
return config
}
// LocalConfig returns a client configuration for local development.
func LocalConfig() *ClientConfig {
config := DefaultConfig()
config.Network = LocalNetwork()
config.KeyringBackend = "test"
return config
}
// LocalAPIConfig returns a client configuration for local API development using localhost.
func LocalAPIConfig() *ClientConfig {
config := DefaultConfig()
config.Network = LocalAPINetwork()
config.KeyringBackend = "test"
return config
}
// TestnetNetwork returns the network configuration for the Sonr testnet.
func TestnetNetwork() NetworkConfig {
return NetworkConfig{
ChainID: "sonrtest_1-1",
Name: "Sonr Testnet",
NetworkID: "testnet",
// TODO: Update these endpoints with actual testnet endpoints
GRPC: "grpc.testnet.sonr.io:443",
REST: "https://api.testnet.sonr.io",
RPC: "https://rpc.testnet.sonr.io",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
GasAdjustment: 1.5,
RequestTimeout: 30 * time.Second,
MaxRetries: 3,
RetryDelay: 1 * time.Second,
Insecure: false,
IPFSGateway: "https://ipfs.testnet.sonr.io",
HighwayAPI: "https://highway.testnet.sonr.io",
}
}
// LocalNetwork returns the network configuration for local development.
func LocalNetwork() NetworkConfig {
return NetworkConfig{
ChainID: "sonrtest_1-1",
Name: "Sonr Local",
NetworkID: "local",
GRPC: "localhost:9090",
REST: "http://localhost:1317",
RPC: "http://localhost:26657",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
GasAdjustment: 1.5,
RequestTimeout: 10 * time.Second,
MaxRetries: 3,
RetryDelay: 500 * time.Millisecond,
Insecure: true, // Allow insecure connections for local development
IPFSGateway: "http://localhost:8080",
HighwayAPI: "http://localhost:8081",
}
}
// LocalAPINetwork returns the network configuration for local API development using localhost.
func LocalAPINetwork() NetworkConfig {
return NetworkConfig{
ChainID: "sonrtest_1-1",
Name: "Sonr Local API",
NetworkID: "local-api",
GRPC: "localhost:9090",
REST: "http://localhost:1317",
RPC: "http://localhost:26657",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
GasAdjustment: 1.5,
RequestTimeout: 10 * time.Second,
MaxRetries: 3,
RetryDelay: 500 * time.Millisecond,
Insecure: true, // Allow insecure connections for local development
IPFSGateway: "http://localhost:8080",
HighwayAPI: "http://localhost:8081",
}
}
// DevnetNetwork returns the network configuration for the development network.
func DevnetNetwork() NetworkConfig {
return NetworkConfig{
ChainID: "sonrtest_1-1",
Name: "Sonr Devnet",
NetworkID: "devnet",
// TODO: Update these endpoints with actual devnet endpoints
GRPC: "grpc.devnet.sonr.io:443",
REST: "https://api.devnet.sonr.io",
RPC: "https://rpc.devnet.sonr.io",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
GasAdjustment: 1.5,
RequestTimeout: 30 * time.Second,
MaxRetries: 3,
RetryDelay: 1 * time.Second,
Insecure: false,
IPFSGateway: "https://ipfs.devnet.sonr.io",
HighwayAPI: "https://highway.devnet.sonr.io",
}
}
// MainnetNetwork returns the network configuration for the Sonr mainnet.
// Note: Mainnet is not yet available.
func MainnetNetwork() NetworkConfig {
return NetworkConfig{
ChainID: "sonr_1-1",
Name: "Sonr Mainnet",
NetworkID: "mainnet",
// TODO: Update these endpoints when mainnet is available
GRPC: "grpc.sonr.io:443",
REST: "https://api.sonr.io",
RPC: "https://rpc.sonr.io",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
GasAdjustment: 1.2,
RequestTimeout: 30 * time.Second,
MaxRetries: 3,
RetryDelay: 2 * time.Second,
Insecure: false,
IPFSGateway: "https://ipfs.sonr.io",
HighwayAPI: "https://highway.sonr.io",
}
}
// Validate checks if the network configuration is valid.
func (nc *NetworkConfig) Validate() error {
if nc.ChainID == "" {
return errors.New("chain ID is required")
}
if nc.GRPC == "" && nc.REST == "" && nc.RPC == "" {
return errors.New("at least one endpoint (GRPC, REST, or RPC) is required")
}
if nc.Denom == "" {
return errors.New("denom is required")
}
if nc.StakingDenom == "" {
return errors.New("staking denom is required")
}
if nc.GasPrice <= 0 {
return errors.New("gas price must be positive")
}
if nc.GasAdjustment <= 0 {
nc.GasAdjustment = 1.5 // Default value
}
if nc.RequestTimeout <= 0 {
nc.RequestTimeout = 30 * time.Second // Default value
}
if nc.MaxRetries < 0 {
nc.MaxRetries = 3 // Default value
}
return nil
}
// Validate checks if the client configuration is valid.
func (cc *ClientConfig) Validate() error {
if err := cc.Network.Validate(); err != nil {
return fmt.Errorf("network config validation failed: %w", err)
}
// Validate keyring backend
validBackends := map[string]bool{
"os": true,
"file": true,
"test": true,
"memory": true,
}
if cc.KeyringBackend != "" && !validBackends[cc.KeyringBackend] {
return fmt.Errorf("invalid keyring backend: %s", cc.KeyringBackend)
}
// Validate log level
validLogLevels := map[string]bool{
"debug": true,
"info": true,
"warn": true,
"error": true,
}
if cc.LogLevel != "" && !validLogLevels[cc.LogLevel] {
return fmt.Errorf("invalid log level: %s", cc.LogLevel)
}
// Validate broadcast mode
validBroadcastModes := map[string]bool{
"sync": true,
"async": true,
"block": true,
}
if cc.BroadcastMode != "" && !validBroadcastModes[cc.BroadcastMode] {
return fmt.Errorf("invalid broadcast mode: %s", cc.BroadcastMode)
}
return nil
}
// IsTestnet returns true if the network is a test network.
func (nc *NetworkConfig) IsTestnet() bool {
return nc.NetworkID == "testnet" || nc.NetworkID == "devnet" || nc.NetworkID == "local" || nc.NetworkID == "local-api"
}
// IsMainnet returns true if the network is the main network.
func (nc *NetworkConfig) IsMainnet() bool {
return nc.NetworkID == "mainnet"
}
// GetNetworkByChainID returns a pre-configured network by chain ID.
func GetNetworkByChainID(chainID string) (NetworkConfig, bool) {
networks := map[string]NetworkConfig{
"sonrtest_1-1": TestnetNetwork(),
"sonr_1-1": MainnetNetwork(),
}
network, exists := networks[chainID]
return network, exists
}
+338
View File
@@ -0,0 +1,338 @@
package config
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestNetworkConfigurations tests pre-configured networks.
func TestNetworkConfigurations(t *testing.T) {
tests := []struct {
name string
network NetworkConfig
expectValid bool
}{
{
name: "testnet config",
network: TestnetNetwork(),
expectValid: true,
},
{
name: "local config",
network: LocalNetwork(),
expectValid: true,
},
{
name: "local API config",
network: LocalAPINetwork(),
expectValid: true,
},
{
name: "devnet config",
network: DevnetNetwork(),
expectValid: true,
},
{
name: "mainnet config",
network: MainnetNetwork(),
expectValid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.network.Validate()
if tt.expectValid {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
// TestNetworkValidation tests network configuration validation.
func TestNetworkValidation(t *testing.T) {
tests := []struct {
name string
config NetworkConfig
wantError bool
errorMsg string
}{
{
name: "valid config",
config: NetworkConfig{
ChainID: "test-chain",
GRPC: "localhost:9090",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
GasAdjustment: 1.5,
RequestTimeout: 30 * time.Second,
},
wantError: false,
},
{
name: "missing chain ID",
config: NetworkConfig{
GRPC: "localhost:9090",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
},
wantError: true,
errorMsg: "chain ID is required",
},
{
name: "no endpoints",
config: NetworkConfig{
ChainID: "test-chain",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
},
wantError: true,
errorMsg: "at least one endpoint",
},
{
name: "missing denom",
config: NetworkConfig{
ChainID: "test-chain",
GRPC: "localhost:9090",
StakingDenom: "usnr",
GasPrice: 0.001,
},
wantError: true,
errorMsg: "denom is required",
},
{
name: "missing staking denom",
config: NetworkConfig{
ChainID: "test-chain",
GRPC: "localhost:9090",
Denom: "snr",
GasPrice: 0.001,
},
wantError: true,
errorMsg: "staking denom is required",
},
{
name: "invalid gas price",
config: NetworkConfig{
ChainID: "test-chain",
GRPC: "localhost:9090",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: -0.001,
},
wantError: true,
errorMsg: "gas price must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if tt.wantError {
require.Error(t, err)
if tt.errorMsg != "" {
require.Contains(t, err.Error(), tt.errorMsg)
}
} else {
require.NoError(t, err)
}
})
}
}
// TestClientConfigValidation tests client configuration validation.
func TestClientConfigValidation(t *testing.T) {
tests := []struct {
name string
config ClientConfig
wantError bool
errorMsg string
}{
{
name: "default config",
config: *DefaultConfig(),
wantError: false,
},
{
name: "testnet config",
config: *TestnetConfig(),
wantError: false,
},
{
name: "local config",
config: *LocalConfig(),
wantError: false,
},
{
name: "invalid keyring backend",
config: ClientConfig{
Network: TestnetNetwork(),
KeyringBackend: "invalid",
},
wantError: true,
errorMsg: "invalid keyring backend",
},
{
name: "invalid log level",
config: ClientConfig{
Network: TestnetNetwork(),
LogLevel: "invalid",
},
wantError: true,
errorMsg: "invalid log level",
},
{
name: "invalid broadcast mode",
config: ClientConfig{
Network: TestnetNetwork(),
BroadcastMode: "invalid",
},
wantError: true,
errorMsg: "invalid broadcast mode",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if tt.wantError {
require.Error(t, err)
if tt.errorMsg != "" {
require.Contains(t, err.Error(), tt.errorMsg)
}
} else {
require.NoError(t, err)
}
})
}
}
// TestIsTestnet tests network type detection.
func TestIsTestnet(t *testing.T) {
tests := []struct {
networkID string
isTestnet bool
}{
{"testnet", true},
{"devnet", true},
{"local", true},
{"local-api", true},
{"mainnet", false},
{"custom", false},
}
for _, tt := range tests {
t.Run(tt.networkID, func(t *testing.T) {
network := NetworkConfig{NetworkID: tt.networkID}
require.Equal(t, tt.isTestnet, network.IsTestnet())
})
}
}
// TestIsMainnet tests mainnet detection.
func TestIsMainnet(t *testing.T) {
tests := []struct {
networkID string
isMainnet bool
}{
{"mainnet", true},
{"testnet", false},
{"devnet", false},
{"local", false},
{"custom", false},
}
for _, tt := range tests {
t.Run(tt.networkID, func(t *testing.T) {
network := NetworkConfig{NetworkID: tt.networkID}
require.Equal(t, tt.isMainnet, network.IsMainnet())
})
}
}
// TestGetNetworkByChainID tests network lookup by chain ID.
func TestGetNetworkByChainID(t *testing.T) {
tests := []struct {
chainID string
expectFound bool
}{
{"sonrtest_1-1", true},
{"sonr_1-1", true},
{"unknown-chain", false},
}
for _, tt := range tests {
t.Run(tt.chainID, func(t *testing.T) {
network, found := GetNetworkByChainID(tt.chainID)
require.Equal(t, tt.expectFound, found)
if found {
require.Equal(t, tt.chainID, network.ChainID)
}
})
}
}
// TestNetworkConfigDefaults tests default value assignment.
func TestNetworkConfigDefaults(t *testing.T) {
cfg := &NetworkConfig{
ChainID: "test-chain",
GRPC: "localhost:9090",
Denom: "snr",
StakingDenom: "usnr",
GasPrice: 0.001,
// Leave defaults unset
GasAdjustment: 0,
RequestTimeout: 0,
MaxRetries: -1,
}
err := cfg.Validate()
require.NoError(t, err)
// Should set defaults
require.Equal(t, 1.5, cfg.GasAdjustment)
require.Equal(t, 30*time.Second, cfg.RequestTimeout)
require.Equal(t, 3, cfg.MaxRetries)
}
// TestConfigurationConsistency tests that all configs are internally consistent.
func TestConfigurationConsistency(t *testing.T) {
configs := []NetworkConfig{
TestnetNetwork(),
LocalNetwork(),
LocalAPINetwork(),
DevnetNetwork(),
MainnetNetwork(),
}
for _, cfg := range configs {
t.Run(cfg.Name, func(t *testing.T) {
// Check required fields
require.NotEmpty(t, cfg.ChainID)
require.NotEmpty(t, cfg.Name)
require.NotEmpty(t, cfg.NetworkID)
require.NotEmpty(t, cfg.Denom)
require.NotEmpty(t, cfg.StakingDenom)
// Check at least one endpoint exists
hasEndpoint := cfg.GRPC != "" || cfg.REST != "" || cfg.RPC != ""
require.True(t, hasEndpoint)
// Check gas configuration
require.Greater(t, cfg.GasPrice, 0.0)
require.Greater(t, cfg.GasAdjustment, 0.0)
// Check timeouts
require.Greater(t, cfg.RequestTimeout, time.Duration(0))
require.GreaterOrEqual(t, cfg.MaxRetries, 0)
require.GreaterOrEqual(t, cfg.RetryDelay, time.Duration(0))
})
}
}
+187
View File
@@ -0,0 +1,187 @@
// Package errors defines error types and utilities for the Sonr client SDK.
package errors
import (
"errors"
"fmt"
sdkerrors "cosmossdk.io/errors"
)
// Common error codes for the Sonr client SDK
const (
// Connection errors
CodeConnectionFailed uint32 = 1001 + iota
CodeInvalidEndpoint
CodeTimeout
CodeNetworkUnreachable
// Authentication errors
CodeInvalidCredentials uint32 = 2001 + iota
CodeKeyNotFound
CodeSigningFailed
CodeWebAuthnFailed
// Transaction errors
CodeInvalidTransaction uint32 = 3001 + iota
CodeInsufficientFunds
CodeGasEstimationFailed
CodeBroadcastFailed
CodeTransactionFailed
// Query errors
CodeQueryFailed uint32 = 4001 + iota
CodeInvalidRequest
CodeNotFound
CodeUnauthorized
// Module-specific errors
CodeDIDError uint32 = 5001 + iota
CodeDWNError
CodeSVCError
CodeUCANError
// Configuration errors
CodeInvalidConfig uint32 = 6001 + iota
CodeMissingConfig
CodeInvalidNetwork
)
var (
// Connection errors
ErrConnectionFailed = sdkerrors.Register("sonr_client", CodeConnectionFailed, "failed to connect to endpoint")
ErrInvalidEndpoint = sdkerrors.Register("sonr_client", CodeInvalidEndpoint, "invalid endpoint configuration")
ErrTimeout = sdkerrors.Register("sonr_client", CodeTimeout, "request timeout")
ErrNetworkUnreachable = sdkerrors.Register("sonr_client", CodeNetworkUnreachable, "network unreachable")
// Authentication errors
ErrInvalidCredentials = sdkerrors.Register("sonr_client", CodeInvalidCredentials, "invalid credentials")
ErrKeyNotFound = sdkerrors.Register("sonr_client", CodeKeyNotFound, "key not found in keyring")
ErrSigningFailed = sdkerrors.Register("sonr_client", CodeSigningFailed, "transaction signing failed")
ErrWebAuthnFailed = sdkerrors.Register("sonr_client", CodeWebAuthnFailed, "WebAuthn operation failed")
// Transaction errors
ErrInvalidTransaction = sdkerrors.Register("sonr_client", CodeInvalidTransaction, "invalid transaction")
ErrInsufficientFunds = sdkerrors.Register("sonr_client", CodeInsufficientFunds, "insufficient funds")
ErrGasEstimationFailed = sdkerrors.Register("sonr_client", CodeGasEstimationFailed, "gas estimation failed")
ErrBroadcastFailed = sdkerrors.Register("sonr_client", CodeBroadcastFailed, "transaction broadcast failed")
ErrTransactionFailed = sdkerrors.Register("sonr_client", CodeTransactionFailed, "transaction execution failed")
// Query errors
ErrQueryFailed = sdkerrors.Register("sonr_client", CodeQueryFailed, "query execution failed")
ErrInvalidRequest = sdkerrors.Register("sonr_client", CodeInvalidRequest, "invalid request parameters")
ErrNotFound = sdkerrors.Register("sonr_client", CodeNotFound, "resource not found")
ErrUnauthorized = sdkerrors.Register("sonr_client", CodeUnauthorized, "unauthorized access")
// Module-specific errors
ErrDIDError = sdkerrors.Register("sonr_client", CodeDIDError, "DID module error")
ErrDWNError = sdkerrors.Register("sonr_client", CodeDWNError, "DWN module error")
ErrSVCError = sdkerrors.Register("sonr_client", CodeSVCError, "SVC module error")
ErrUCANError = sdkerrors.Register("sonr_client", CodeUCANError, "UCAN module error")
// Configuration errors
ErrInvalidConfig = sdkerrors.Register("sonr_client", CodeInvalidConfig, "invalid configuration")
ErrMissingConfig = sdkerrors.Register("sonr_client", CodeMissingConfig, "missing required configuration")
ErrInvalidNetwork = sdkerrors.Register("sonr_client", CodeInvalidNetwork, "invalid network configuration")
)
// WrapError wraps an existing error with additional context and a Sonr-specific error code.
// This follows the Cosmos SDK pattern for error handling.
func WrapError(err error, sdkErr *sdkerrors.Error, format string, args ...any) error {
if err == nil {
return nil
}
msg := fmt.Sprintf(format, args...)
return sdkerrors.Wrapf(sdkErr, "%s: %v", msg, err)
}
// IsConnectionError returns true if the error is related to network connectivity.
func IsConnectionError(err error) bool {
return errors.Is(err, ErrConnectionFailed) ||
errors.Is(err, ErrInvalidEndpoint) ||
errors.Is(err, ErrTimeout) ||
errors.Is(err, ErrNetworkUnreachable)
}
// IsAuthenticationError returns true if the error is related to authentication.
func IsAuthenticationError(err error) bool {
return errors.Is(err, ErrInvalidCredentials) ||
errors.Is(err, ErrKeyNotFound) ||
errors.Is(err, ErrSigningFailed) ||
errors.Is(err, ErrWebAuthnFailed)
}
// IsTransactionError returns true if the error is related to transaction processing.
func IsTransactionError(err error) bool {
return errors.Is(err, ErrInvalidTransaction) ||
errors.Is(err, ErrInsufficientFunds) ||
errors.Is(err, ErrGasEstimationFailed) ||
errors.Is(err, ErrBroadcastFailed) ||
errors.Is(err, ErrTransactionFailed)
}
// IsQueryError returns true if the error is related to query operations.
func IsQueryError(err error) bool {
return errors.Is(err, ErrQueryFailed) ||
errors.Is(err, ErrInvalidRequest) ||
errors.Is(err, ErrNotFound) ||
errors.Is(err, ErrUnauthorized)
}
// IsConfigurationError returns true if the error is related to configuration.
func IsConfigurationError(err error) bool {
return errors.Is(err, ErrInvalidConfig) ||
errors.Is(err, ErrMissingConfig) ||
errors.Is(err, ErrInvalidNetwork)
}
// GetErrorCode extracts the error code from a Cosmos SDK error.
// Returns 0 if the error is not a Cosmos SDK error or doesn't have a code.
func GetErrorCode(err error) uint32 {
var sdkErr *sdkerrors.Error
if errors.As(err, &sdkErr) {
return sdkErr.ABCICode()
}
return 0
}
// NewConnectionError creates a new connection-related error with context.
func NewConnectionError(endpoint string, underlying error) error {
return WrapError(underlying, ErrConnectionFailed, "failed to connect to %s", endpoint)
}
// NewAuthenticationError creates a new authentication-related error with context.
func NewAuthenticationError(operation string, underlying error) error {
return WrapError(underlying, ErrInvalidCredentials, "authentication failed for %s", operation)
}
// NewTransactionError creates a new transaction-related error with context.
func NewTransactionError(txHash string, underlying error) error {
return WrapError(underlying, ErrTransactionFailed, "transaction %s failed", txHash)
}
// NewQueryError creates a new query-related error with context.
func NewQueryError(query string, underlying error) error {
return WrapError(underlying, ErrQueryFailed, "query %s failed", query)
}
// NewModuleError creates a new module-specific error with context.
func NewModuleError(module string, operation string, underlying error) error {
var baseErr *sdkerrors.Error
switch module {
case "did":
baseErr = ErrDIDError
case "dwn":
baseErr = ErrDWNError
case "svc":
baseErr = ErrSVCError
case "ucan":
baseErr = ErrUCANError
default:
baseErr = ErrQueryFailed
}
return WrapError(underlying, baseErr, "%s module %s operation failed", module, operation)
}
+225
View File
@@ -0,0 +1,225 @@
module github.com/sonr-io/sonr/client
go 1.24.7
// Replace directive to use the parent module's DWN plugin
replace github.com/sonr-io/sonr => ../
// Inherit necessary replace directives from parent module
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/99designs/keyring => github.com/cosmos/keyring v1.2.0
github.com/cosmos/cosmos-sdk => github.com/strangelove-ventures/cosmos-sdk v0.0.0-20250317212103-0767f8c5b1e5
github.com/sonr-io/sonr/crypto => ../crypto
github.com/spf13/viper => github.com/spf13/viper v1.17.0
nhooyr.io/websocket => nhooyr.io/websocket v1.8.17
)
require (
cosmossdk.io/errors v1.0.1
cosmossdk.io/math v1.5.0
github.com/cosmos/cosmos-sdk v0.53.4
github.com/sonr-io/sonr v0.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.10.0
google.golang.org/grpc v1.71.0
)
require (
cosmossdk.io/api v0.7.6 // indirect
cosmossdk.io/collections v0.4.0 // indirect
cosmossdk.io/core v0.12.0 // indirect
cosmossdk.io/depinject v1.1.0 // indirect
cosmossdk.io/log v1.5.0 // indirect
cosmossdk.io/orm v1.0.0-beta.3 // indirect
cosmossdk.io/store v1.1.1 // indirect
cosmossdk.io/x/tx v0.13.7 // 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/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/Workiva/go-datastructures v1.1.3 // indirect
github.com/asynkron/protoactor-go v0.0.0-20240822202345-3c0e61ca19c9 // indirect
github.com/beorn7/perks v1.0.1 // 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.24.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/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // 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/redact v1.1.5 // 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/gnark-crypto v0.19.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/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/ics23/go v0.11.0 // indirect
github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // 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/extism/go-sdk v1.7.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.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/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/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/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/uuid v1.6.0 // 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-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/golang-lru v1.0.2 // 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/huandu/skiplist v1.2.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/go-cid v0.5.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/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/libp2p/go-libp2p v0.43.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/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.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-multibase v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.1.0 // 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/orcaman/concurrent-map v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // 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.6 // indirect
github.com/spf13/viper v1.19.0 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
github.com/x448/float16 v0.8.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/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/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.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/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.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
sigs.k8s.io/yaml v1.5.0 // indirect
)
+1082
View File
File diff suppressed because it is too large Load Diff
+355
View File
@@ -0,0 +1,355 @@
// Package keys provides key management functionality for the Sonr client SDK.
// This package integrates with Sonr's DWN plugin architecture for Decentralized Abstracted Smart Wallets.
package keys
import (
"context"
"fmt"
"time"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// KeyringManager provides an interface for managing Decentralized Abstracted Smart Wallets
// through the Sonr DWN plugin architecture.
type KeyringManager interface {
// Wallet Identity Operations
GetIssuerDID(ctx context.Context) (*WalletIdentity, error)
GetAddress(ctx context.Context) (string, error)
// UCAN Token Operations for Authorization
CreateOriginToken(ctx context.Context, req *UCANRequest) (*UCANToken, error)
CreateAttenuatedToken(ctx context.Context, req *AttenuatedUCANRequest) (*UCANToken, error)
// Signing Operations using MPC
Sign(ctx context.Context, data []byte) (*Signature, error)
SignTransaction(ctx context.Context, txBytes []byte) (*Signature, error)
// Verification Operations
Verify(ctx context.Context, data []byte, signature []byte) (bool, error)
// Plugin Management
Plugin() plugin.Plugin
Close() error
}
// WalletIdentity represents the identity of a Decentralized Abstracted Smart Wallet.
type WalletIdentity struct {
DID string `json:"did"` // W3C DID identifier
Address string `json:"address"` // Sonr blockchain address
ChainCode string `json:"chain_code"` // Deterministic chain code
}
// UCANRequest represents a request to create a UCAN origin token.
type UCANRequest struct {
AudienceDID string `json:"audience_did"` // Target audience DID
Capabilities []map[string]any `json:"capabilities,omitempty"` // Granted capabilities
Facts []string `json:"facts,omitempty"` // Additional facts
NotBefore *time.Time `json:"not_before,omitempty"` // Validity start time
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Expiration time
}
// AttenuatedUCANRequest represents a request to create an attenuated UCAN token.
type AttenuatedUCANRequest struct {
ParentToken string `json:"parent_token"` // Parent UCAN token
AudienceDID string `json:"audience_did"` // Target audience DID
Capabilities []map[string]any `json:"capabilities,omitempty"` // Attenuated capabilities
Facts []string `json:"facts,omitempty"` // Additional facts
NotBefore *time.Time `json:"not_before,omitempty"` // Validity start time
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Expiration time
}
// UCANToken represents a User-Controlled Authorization Network token.
type UCANToken struct {
Token string `json:"token"` // The UCAN JWT token
Issuer string `json:"issuer"` // Issuer DID
Address string `json:"address"` // Issuer address
}
// Signature represents a cryptographic signature.
type Signature struct {
Signature []byte `json:"signature"` // Signature bytes
Algorithm string `json:"algorithm"` // Signature algorithm used
}
// keyringManager implements KeyringManager using the DWN plugin architecture.
type keyringManager struct {
plugin plugin.Plugin
chainID string
}
// KeyringOptions configures keyring creation for Decentralized Abstracted Smart Wallets.
type KeyringOptions struct {
ChainID string `json:"chain_id"`
EnclaveData []byte `json:"enclave_data"` // JSON-encoded MPC enclave data
VaultConfig map[string]any `json:"vault_config,omitempty"`
UseManager bool `json:"use_manager,omitempty"` // Use plugin manager for production
}
// NewKeyringManager creates a new keyring manager using the DWN plugin architecture.
func NewKeyringManager(backend, dir, chainID string) (KeyringManager, error) {
if chainID == "" {
return nil, fmt.Errorf("chain ID is required")
}
// For backwards compatibility, create with minimal config
// In practice, clients should use NewKeyringManagerWithOptions
opts := KeyringOptions{
ChainID: chainID,
EnclaveData: []byte(`{}`), // Minimal enclave data
UseManager: false, // Use simple plugin loading
}
return NewKeyringManagerWithOptions(opts)
}
// NewKeyringManagerWithOptions creates a new keyring manager with detailed options
// for Decentralized Abstracted Smart Wallets.
func NewKeyringManagerWithOptions(opts KeyringOptions) (KeyringManager, error) {
if opts.ChainID == "" {
return nil, fmt.Errorf("chain ID is required")
}
ctx := context.Background()
var p plugin.Plugin
var err error
if opts.UseManager {
// For production use with plugin manager, we would need to properly construct
// the EnclaveConfig with the correct types. For now, fall back to simple loading.
// TODO: Implement proper EnclaveConfig construction when plugin manager is ready
p, err = plugin.LoadPluginWithEnclave(ctx, opts.ChainID, opts.EnclaveData, opts.VaultConfig)
} else {
// Use simple plugin loading for development
p, err = plugin.LoadPluginWithEnclave(ctx, opts.ChainID, opts.EnclaveData, opts.VaultConfig)
}
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to load DWN plugin for chain %s", opts.ChainID)
}
return &keyringManager{
plugin: p,
chainID: opts.ChainID,
}, nil
}
// GetIssuerDID retrieves the wallet's DID identity information.
func (km *keyringManager) GetIssuerDID(ctx context.Context) (*WalletIdentity, error) {
resp, err := km.plugin.GetIssuerDID()
if err != nil {
return nil, errors.WrapError(err, errors.ErrKeyNotFound, "failed to get issuer DID from wallet")
}
if resp.Error != "" {
return nil, fmt.Errorf("plugin error: %s", resp.Error)
}
return &WalletIdentity{
DID: resp.IssuerDID,
Address: resp.Address,
ChainCode: resp.ChainCode,
}, nil
}
// GetAddress retrieves the wallet's blockchain address.
func (km *keyringManager) GetAddress(ctx context.Context) (string, error) {
identity, err := km.GetIssuerDID(ctx)
if err != nil {
return "", err
}
return identity.Address, nil
}
// CreateOriginToken creates a new UCAN origin token for authorization.
func (km *keyringManager) CreateOriginToken(ctx context.Context, req *UCANRequest) (*UCANToken, error) {
// Convert to plugin request format
pluginReq := &plugin.NewOriginTokenRequest{
AudienceDID: req.AudienceDID,
Attenuations: req.Capabilities,
Facts: req.Facts,
}
if req.NotBefore != nil {
pluginReq.NotBefore = req.NotBefore.Unix()
}
if req.ExpiresAt != nil {
pluginReq.ExpiresAt = req.ExpiresAt.Unix()
}
resp, err := km.plugin.NewOriginToken(pluginReq)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to create origin UCAN token")
}
if resp.Error != "" {
return nil, fmt.Errorf("plugin error: %s", resp.Error)
}
return &UCANToken{
Token: resp.Token,
Issuer: resp.Issuer,
Address: resp.Address,
}, nil
}
// CreateAttenuatedToken creates an attenuated UCAN token by delegating from a parent token.
func (km *keyringManager) CreateAttenuatedToken(ctx context.Context, req *AttenuatedUCANRequest) (*UCANToken, error) {
// Convert to plugin request format
pluginReq := &plugin.NewAttenuatedTokenRequest{
ParentToken: req.ParentToken,
AudienceDID: req.AudienceDID,
Attenuations: req.Capabilities,
Facts: req.Facts,
}
if req.NotBefore != nil {
pluginReq.NotBefore = req.NotBefore.Unix()
}
if req.ExpiresAt != nil {
pluginReq.ExpiresAt = req.ExpiresAt.Unix()
}
resp, err := km.plugin.NewAttenuatedToken(pluginReq)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to create attenuated UCAN token")
}
if resp.Error != "" {
return nil, fmt.Errorf("plugin error: %s", resp.Error)
}
return &UCANToken{
Token: resp.Token,
Issuer: resp.Issuer,
Address: resp.Address,
}, nil
}
// Sign signs arbitrary data using the MPC-based wallet.
func (km *keyringManager) Sign(ctx context.Context, data []byte) (*Signature, error) {
req := &plugin.SignDataRequest{
Data: data,
}
resp, err := km.plugin.SignData(req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to sign data")
}
if resp.Error != "" {
return nil, fmt.Errorf("plugin error: %s", resp.Error)
}
return &Signature{
Signature: resp.Signature,
Algorithm: "MPC", // The plugin uses MPC-based signing
}, nil
}
// SignTransaction signs a transaction using the MPC-based wallet.
func (km *keyringManager) SignTransaction(ctx context.Context, txBytes []byte) (*Signature, error) {
// Transaction signing uses the same underlying data signing mechanism
return km.Sign(ctx, txBytes)
}
// Verify verifies a signature against data using the MPC-based wallet.
func (km *keyringManager) Verify(ctx context.Context, data []byte, signature []byte) (bool, error) {
req := &plugin.VerifyDataRequest{
Data: data,
Signature: signature,
}
resp, err := km.plugin.VerifyData(req)
if err != nil {
return false, errors.WrapError(err, errors.ErrSigningFailed, "failed to verify signature")
}
if resp.Error != "" {
return false, fmt.Errorf("plugin error: %s", resp.Error)
}
return resp.Valid, nil
}
// Plugin returns the underlying DWN plugin for advanced operations.
func (km *keyringManager) Plugin() plugin.Plugin {
return km.plugin
}
// Close closes the keyring manager and releases resources.
func (km *keyringManager) Close() error {
// Note: The plugin interface doesn't currently expose a Close method
// This is here for future compatibility and to satisfy the interface
return nil
}
// Utility functions for working with Sonr addresses and DIDs
// SonrBech32Prefix returns the bech32 prefix used by Sonr addresses.
func SonrBech32Prefix() string {
return "sonr"
}
// WalletInfo provides formatted information about a Decentralized Abstracted Smart Wallet.
type WalletInfo struct {
DID string `json:"did"`
Address string `json:"address"`
ChainCode string `json:"chain_code"`
ChainID string `json:"chain_id"`
}
// GetWalletInfo returns formatted information about the wallet.
func (km *keyringManager) GetWalletInfo(ctx context.Context) (*WalletInfo, error) {
identity, err := km.GetIssuerDID(ctx)
if err != nil {
return nil, err
}
return &WalletInfo{
DID: identity.DID,
Address: identity.Address,
ChainCode: identity.ChainCode,
ChainID: km.chainID,
}, nil
}
// CreateDefaultUCANRequest creates a UCAN request with sensible defaults.
func CreateDefaultUCANRequest(audienceDID string) *UCANRequest {
// Create a token that expires in 1 hour with basic capabilities
expiresAt := time.Now().Add(time.Hour)
return &UCANRequest{
AudienceDID: audienceDID,
Capabilities: []map[string]any{
{
"can": []string{"sign", "verify"},
"with": "vault://default",
},
},
ExpiresAt: &expiresAt,
}
}
// CreateTransactionUCANRequest creates a UCAN request specifically for transaction signing.
func CreateTransactionUCANRequest(audienceDID string, txHash string) *UCANRequest {
// Create a short-lived token specifically for transaction signing
expiresAt := time.Now().Add(10 * time.Minute)
return &UCANRequest{
AudienceDID: audienceDID,
Capabilities: []map[string]any{
{
"can": []string{"sign"},
"with": fmt.Sprintf("tx://%s", txHash),
},
},
Facts: []string{
fmt.Sprintf("transaction:%s", txHash),
},
ExpiresAt: &expiresAt,
}
}
+606
View File
@@ -0,0 +1,606 @@
// Package did provides a client interface for interacting with the Sonr DID module.
package did
import (
"context"
"fmt"
"strings"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
didtypes "github.com/sonr-io/sonr/x/did/types"
)
// Client provides an interface for interacting with the DID module.
type Client interface {
// DID Operations
CreateDID(ctx context.Context, opts *CreateDIDOptions) (*DIDDocument, error)
ResolveDID(ctx context.Context, did string) (*DIDDocument, error)
UpdateDID(ctx context.Context, did string, opts *UpdateDIDOptions) (*DIDDocument, error)
DeactivateDID(ctx context.Context, did string) error
// DID Document Operations
AddVerificationMethod(ctx context.Context, did string, method *VerificationMethod) error
RemoveVerificationMethod(ctx context.Context, did string, methodID string) error
AddService(ctx context.Context, did string, service *Service) error
RemoveService(ctx context.Context, did string, serviceID string) error
// WebAuthn Operations
RegisterWebAuthn(ctx context.Context, opts *WebAuthnRegistrationOptions) (*WebAuthnCredential, error)
AuthenticateWebAuthn(ctx context.Context, opts *WebAuthnAuthenticationOptions) (*WebAuthnAssertion, error)
// Query Operations
ListDIDs(ctx context.Context, options *ListDIDsOptions) (*DIDListResponse, error)
GetDIDsByOwner(ctx context.Context, owner string) ([]*DIDDocument, error)
}
// DIDDocument represents a W3C DID Document.
type DIDDocument struct {
ID string `json:"id"`
Controller []string `json:"controller,omitempty"`
VerificationMethod []*VerificationMethod `json:"verificationMethod,omitempty"`
Authentication []string `json:"authentication,omitempty"`
AssertionMethod []string `json:"assertionMethod,omitempty"`
KeyAgreement []string `json:"keyAgreement,omitempty"`
CapabilityInvocation []string `json:"capabilityInvocation,omitempty"`
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
Service []*Service `json:"service,omitempty"`
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
Metadata *DIDMetadata `json:"metadata,omitempty"`
}
// VerificationMethod represents a verification method in a DID document.
type VerificationMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyJwk map[string]any `json:"publicKeyJwk,omitempty"`
PublicKeyMultibase string `json:"publicKeyMultibase,omitempty"`
}
// Service represents a service endpoint in a DID document.
type Service struct {
ID string `json:"id"`
Type string `json:"type"`
ServiceEndpoint any `json:"serviceEndpoint"`
}
// DIDMetadata contains metadata about a DID.
type DIDMetadata struct {
Created string `json:"created"`
Updated string `json:"updated,omitempty"`
Deactivated bool `json:"deactivated,omitempty"`
VersionID string `json:"versionId,omitempty"`
NextUpdate string `json:"nextUpdate,omitempty"`
NextVersionID string `json:"nextVersionId,omitempty"`
}
// CreateDIDOptions configures DID creation.
type CreateDIDOptions struct {
Controller []string `json:"controller,omitempty"`
VerificationMethods []*VerificationMethod `json:"verificationMethods,omitempty"`
Services []*Service `json:"services,omitempty"`
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
UseWebAuthn bool `json:"useWebAuthn,omitempty"`
}
// UpdateDIDOptions configures DID updates.
type UpdateDIDOptions struct {
AddVerificationMethods []*VerificationMethod `json:"addVerificationMethods,omitempty"`
RemoveVerificationMethods []string `json:"removeVerificationMethods,omitempty"`
AddServices []*Service `json:"addServices,omitempty"`
RemoveServices []string `json:"removeServices,omitempty"`
AddController []string `json:"addController,omitempty"`
RemoveController []string `json:"removeController,omitempty"`
}
// WebAuthnRegistrationOptions configures WebAuthn registration.
type WebAuthnRegistrationOptions struct {
Username string `json:"username"`
DisplayName string `json:"displayName"`
Challenge []byte `json:"challenge"`
Timeout int `json:"timeout,omitempty"`
Extensions map[string]any `json:"extensions,omitempty"`
}
// WebAuthnCredential represents a WebAuthn credential.
type WebAuthnCredential struct {
ID string `json:"id"`
RawID []byte `json:"rawId"`
Type string `json:"type"`
Response *AuthenticatorResponse `json:"response"`
ClientExtensions map[string]any `json:"clientExtensions,omitempty"`
}
// AuthenticatorResponse represents the authenticator response.
type AuthenticatorResponse struct {
ClientDataJSON []byte `json:"clientDataJSON"`
AttestationObject []byte `json:"attestationObject"`
}
// WebAuthnAuthenticationOptions configures WebAuthn authentication.
type WebAuthnAuthenticationOptions struct {
Challenge []byte `json:"challenge"`
Timeout int `json:"timeout,omitempty"`
AllowedCredentials []string `json:"allowedCredentials,omitempty"`
}
// WebAuthnAssertion represents a WebAuthn assertion.
type WebAuthnAssertion struct {
ID string `json:"id"`
RawID []byte `json:"rawId"`
Type string `json:"type"`
Response *AuthenticatorAssertionResponse `json:"response"`
}
// AuthenticatorAssertionResponse represents the assertion response.
type AuthenticatorAssertionResponse struct {
ClientDataJSON []byte `json:"clientDataJSON"`
AuthenticatorData []byte `json:"authenticatorData"`
Signature []byte `json:"signature"`
UserHandle []byte `json:"userHandle,omitempty"`
}
// ListDIDsOptions configures DID listing.
type ListDIDsOptions struct {
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
Owner string `json:"owner,omitempty"`
}
// DIDListResponse contains a list of DIDs with pagination.
type DIDListResponse struct {
DIDs []*DIDDocument `json:"dids"`
TotalCount uint64 `json:"totalCount"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// client implements the DID Client interface.
type client struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
// Service clients for DID module
queryClient didtypes.QueryClient
msgClient didtypes.MsgClient
txClient tx.ServiceClient
}
// NewClient creates a new DID module client.
func NewClient(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) Client {
return &client{
grpcConn: grpcConn,
config: cfg,
queryClient: didtypes.NewQueryClient(grpcConn),
msgClient: didtypes.NewMsgClient(grpcConn),
txClient: tx.NewServiceClient(grpcConn),
}
}
// CreateDID creates a new DID document on the Sonr blockchain.
func (c *client) CreateDID(ctx context.Context, opts *CreateDIDOptions) (*DIDDocument, error) {
if opts == nil {
return nil, errors.NewModuleError("did", "CreateDID",
fmt.Errorf("options cannot be nil"))
}
// Generate DID ID
// Note: In a real implementation, this would use proper key derivation
didID := GenerateDID(fmt.Sprintf("user_%d", len(opts.Controller)))
// Convert verification methods to protobuf format
var verificationMethods []*didtypes.VerificationMethod
for _, vm := range opts.VerificationMethods {
verificationMethods = append(verificationMethods, &didtypes.VerificationMethod{
Id: vm.ID,
VerificationMethodKind: vm.Type,
Controller: vm.Controller,
PublicKeyMultibase: vm.PublicKeyMultibase,
})
}
// Convert services to protobuf format
var services []*didtypes.Service
for _, svc := range opts.Services {
services = append(services, &didtypes.Service{
Id: svc.ID,
ServiceKind: svc.Type,
SingleEndpoint: fmt.Sprintf("%v", svc.ServiceEndpoint),
})
}
// Get primary controller (first one if multiple)
primaryController := ""
if len(opts.Controller) > 0 {
primaryController = opts.Controller[0]
}
// Create DID Document
didDocument := didtypes.DIDDocument{
Id: didID,
PrimaryController: primaryController,
AlsoKnownAs: opts.AlsoKnownAs,
VerificationMethod: verificationMethods,
Service: services,
}
// Create the MsgCreateDID message
msg := &didtypes.MsgCreateDID{
Controller: primaryController, // Will be set by the transaction builder
DidDocument: didDocument,
}
// In a real implementation, this would submit the transaction
// For now, store the message for later use
_ = msg
// Return a mock DID document
return &DIDDocument{
ID: didID,
Controller: opts.Controller,
VerificationMethod: opts.VerificationMethods,
Service: opts.Services,
AlsoKnownAs: opts.AlsoKnownAs,
Metadata: &DIDMetadata{
Created: "2024-01-01T00:00:00Z",
},
}, nil
}
// ResolveDID resolves a DID to its document.
func (c *client) ResolveDID(ctx context.Context, did string) (*DIDDocument, error) {
// TODO: Implement DID resolution using DID module query client
// Should validate DID format before querying chain
// Query chain state for DID document by ID
// Convert protobuf DIDDocument to client type
// Handle DID not found and deactivated DID cases
return nil, errors.NewModuleError("did", "ResolveDID",
fmt.Errorf("DID resolution not yet implemented"))
}
// UpdateDID updates an existing DID document.
func (c *client) UpdateDID(ctx context.Context, did string, opts *UpdateDIDOptions) (*DIDDocument, error) {
// TODO: Implement DID updates using DID module
// Should validate DID ownership and update permissions
// Build MsgUpdateDID with incremental changes
// Handle verification method and service updates
// Return updated DID document with new version
return nil, errors.NewModuleError("did", "UpdateDID",
fmt.Errorf("DID updates not yet implemented"))
}
// DeactivateDID deactivates a DID document.
func (c *client) DeactivateDID(ctx context.Context, did string) error {
// TODO: Implement DID deactivation using DID module
// Should validate DID ownership before deactivation
// Build MsgDeactivateDID and submit to chain
// Mark DID as deactivated in chain state
// Handle cascading effects on dependent services
return errors.NewModuleError("did", "DeactivateDID",
fmt.Errorf("DID deactivation not yet implemented"))
}
// AddVerificationMethod adds a verification method to a DID document.
func (c *client) AddVerificationMethod(ctx context.Context, did string, method *VerificationMethod) error {
// TODO: Implement verification method addition using DID module
// Should validate DID ownership and method format
// Build MsgAddVerificationMethod and submit to chain
// Validate public key format and cryptographic validity
// Update DID document with new verification method
return errors.NewModuleError("did", "AddVerificationMethod",
fmt.Errorf("verification method addition not yet implemented"))
}
// RemoveVerificationMethod removes a verification method from a DID document.
func (c *client) RemoveVerificationMethod(ctx context.Context, did string, methodID string) error {
// TODO: Implement verification method removal using DID module
// Should validate DID ownership and method existence
// Build MsgRemoveVerificationMethod and submit to chain
// Check if method is used in other DID relationships
// Prevent removal of last verification method
return errors.NewModuleError("did", "RemoveVerificationMethod",
fmt.Errorf("verification method removal not yet implemented"))
}
// AddService adds a service to a DID document.
func (c *client) AddService(ctx context.Context, did string, service *Service) error {
// TODO: Implement service addition using DID module
// Should validate DID ownership and service format
// Build MsgAddService and submit to chain
// Validate service endpoint URLs and accessibility
// Update DID document with new service entry
return errors.NewModuleError("did", "AddService",
fmt.Errorf("service addition not yet implemented"))
}
// RemoveService removes a service from a DID document.
func (c *client) RemoveService(ctx context.Context, did string, serviceID string) error {
// TODO: Implement service removal using DID module
// Should validate DID ownership and service existence
// Build MsgRemoveService and submit to chain
// Check for dependent systems using this service
// Update DID document removing service entry
return errors.NewModuleError("did", "RemoveService",
fmt.Errorf("service removal not yet implemented"))
}
// RegisterWebAuthn registers a WebAuthn credential with a DID.
func (c *client) RegisterWebAuthn(ctx context.Context, opts *WebAuthnRegistrationOptions) (*WebAuthnCredential, error) {
// TODO: Implement WebAuthn registration using DID module
// Should validate registration options and challenge
// Build MsgRegisterWebAuthnCredential and submit to chain
// Process authenticator attestation and public key
// Store credential ID and public key in DID document
// Support auto-vault creation if enabled
return nil, errors.NewModuleError("did", "RegisterWebAuthn",
fmt.Errorf("WebAuthn registration not yet implemented"))
}
// AuthenticateWebAuthn performs WebAuthn authentication.
func (c *client) AuthenticateWebAuthn(ctx context.Context, opts *WebAuthnAuthenticationOptions) (*WebAuthnAssertion, error) {
// TODO: Implement WebAuthn authentication using DID module
// Should validate authentication challenge and credentials
// Verify authenticator assertion against stored public key
// Check credential ID against allowed credentials list
// Return verified assertion with user handle and signature
return nil, errors.NewModuleError("did", "AuthenticateWebAuthn",
fmt.Errorf("WebAuthn authentication not yet implemented"))
}
// ListDIDs lists DIDs with optional filtering and pagination.
func (c *client) ListDIDs(ctx context.Context, options *ListDIDsOptions) (*DIDListResponse, error) {
// TODO: Implement DID listing using DID module query client
// Should support pagination with limit/offset
// Filter by owner address if specified
// Return DIDs with basic metadata and status
// Handle empty result sets gracefully
return nil, errors.NewModuleError("did", "ListDIDs",
fmt.Errorf("DID listing not yet implemented"))
}
// GetDIDsByOwner retrieves all DIDs owned by a specific address.
func (c *client) GetDIDsByOwner(ctx context.Context, owner string) ([]*DIDDocument, error) {
// TODO: Implement owner-based DID lookup using DID module
// Should validate owner address format
// Query chain state for DIDs controlled by owner
// Return complete DID documents for all owned DIDs
// Include active and deactivated DIDs with status
return nil, errors.NewModuleError("did", "GetDIDsByOwner",
fmt.Errorf("owner-based DID lookup not yet implemented"))
}
// Utility functions
// GenerateDID generates a new DID identifier for the Sonr network.
func GenerateDID(identifier string) string {
return fmt.Sprintf("did:sonr:%s", identifier)
}
// ValidateDID validates a DID format.
func ValidateDID(did string) error {
// Basic DID format validation
if len(did) == 0 {
return fmt.Errorf("DID cannot be empty")
}
if !strings.HasPrefix(did, "did:sonr:") {
return fmt.Errorf("DID must start with 'did:sonr:'")
}
return nil
}
// CreateDefaultVerificationMethod creates a default verification method.
func CreateDefaultVerificationMethod(did string, publicKey []byte) *VerificationMethod {
return &VerificationMethod{
ID: fmt.Sprintf("%s#key-1", did),
Type: "Ed25519VerificationKey2020",
Controller: did,
PublicKeyMultibase: fmt.Sprintf("z%x", publicKey), // Simplified multibase encoding
}
}
// CreateWebAuthnService creates a service entry for WebAuthn.
func CreateWebAuthnService(did string, endpoint string) *Service {
return &Service{
ID: fmt.Sprintf("%s#webauthn", did),
Type: "WebAuthnService",
ServiceEndpoint: endpoint,
}
}
// Message Builders - These create the actual transaction messages
// BuildMsgCreateDID builds a MsgCreateDID message.
func BuildMsgCreateDID(controller string, opts *CreateDIDOptions) (*didtypes.MsgCreateDID, error) {
if opts == nil {
return nil, fmt.Errorf("options cannot be nil")
}
// Generate DID ID
didID := GenerateDID(fmt.Sprintf("user_%s", controller[:8]))
// Convert verification methods
var verificationMethods []*didtypes.VerificationMethod
for _, vm := range opts.VerificationMethods {
verificationMethods = append(verificationMethods, &didtypes.VerificationMethod{
Id: vm.ID,
VerificationMethodKind: vm.Type,
Controller: vm.Controller,
PublicKeyMultibase: vm.PublicKeyMultibase,
})
}
// Convert services
var services []*didtypes.Service
for _, svc := range opts.Services {
services = append(services, &didtypes.Service{
Id: svc.ID,
ServiceKind: svc.Type,
SingleEndpoint: fmt.Sprintf("%v", svc.ServiceEndpoint),
})
}
// Create DID Document
didDocument := didtypes.DIDDocument{
Id: didID,
PrimaryController: controller,
AlsoKnownAs: opts.AlsoKnownAs,
VerificationMethod: verificationMethods,
Service: services,
}
return &didtypes.MsgCreateDID{
Controller: controller,
DidDocument: didDocument,
}, nil
}
// BuildMsgUpdateDID builds a MsgUpdateDID message.
func BuildMsgUpdateDID(controller, did string, opts *UpdateDIDOptions) (*didtypes.MsgUpdateDID, error) {
if opts == nil {
return nil, fmt.Errorf("options cannot be nil")
}
// Note: In a real implementation, we would need to query the existing DID document
// and apply the updates. For now, we create a minimal DID document with updates.
// Convert new verification methods
var verificationMethods []*didtypes.VerificationMethod
for _, vm := range opts.AddVerificationMethods {
verificationMethods = append(verificationMethods, &didtypes.VerificationMethod{
Id: vm.ID,
VerificationMethodKind: vm.Type,
Controller: vm.Controller,
PublicKeyMultibase: vm.PublicKeyMultibase,
})
}
// Convert new services
var services []*didtypes.Service
for _, svc := range opts.AddServices {
services = append(services, &didtypes.Service{
Id: svc.ID,
ServiceKind: svc.Type,
SingleEndpoint: fmt.Sprintf("%v", svc.ServiceEndpoint),
})
}
// Create updated DID Document
didDocument := didtypes.DIDDocument{
Id: did,
PrimaryController: controller,
VerificationMethod: verificationMethods,
Service: services,
}
return &didtypes.MsgUpdateDID{
Controller: controller,
Did: did,
DidDocument: didDocument,
}, nil
}
// BuildMsgDeactivateDID builds a MsgDeactivateDID message.
func BuildMsgDeactivateDID(controller, did string) *didtypes.MsgDeactivateDID {
return &didtypes.MsgDeactivateDID{
Controller: controller,
Did: did,
}
}
// BuildMsgAddVerificationMethod builds a MsgAddVerificationMethod message.
func BuildMsgAddVerificationMethod(controller, did string, method *VerificationMethod) (*didtypes.MsgAddVerificationMethod, error) {
if method == nil {
return nil, fmt.Errorf("verification method cannot be nil")
}
return &didtypes.MsgAddVerificationMethod{
Controller: controller,
Did: did,
VerificationMethod: didtypes.VerificationMethod{
Id: method.ID,
VerificationMethodKind: method.Type,
Controller: method.Controller,
PublicKeyMultibase: method.PublicKeyMultibase,
},
}, nil
}
// BuildMsgRemoveVerificationMethod builds a MsgRemoveVerificationMethod message.
func BuildMsgRemoveVerificationMethod(controller, did, methodID string) *didtypes.MsgRemoveVerificationMethod {
return &didtypes.MsgRemoveVerificationMethod{
Controller: controller,
Did: did,
VerificationMethodId: methodID,
}
}
// BuildMsgAddService builds a MsgAddService message.
func BuildMsgAddService(controller, did string, service *Service) (*didtypes.MsgAddService, error) {
if service == nil {
return nil, fmt.Errorf("service cannot be nil")
}
return &didtypes.MsgAddService{
Controller: controller,
Did: did,
Service: didtypes.Service{
Id: service.ID,
ServiceKind: service.Type,
SingleEndpoint: fmt.Sprintf("%v", service.ServiceEndpoint),
},
}, nil
}
// BuildMsgRemoveService builds a MsgRemoveService message.
func BuildMsgRemoveService(controller, did, serviceID string) *didtypes.MsgRemoveService {
return &didtypes.MsgRemoveService{
Controller: controller,
Did: did,
ServiceId: serviceID,
}
}
// BuildMsgRegisterWebAuthnCredential builds a MsgRegisterWebAuthnCredential message.
func BuildMsgRegisterWebAuthnCredential(controller, username string, credential *WebAuthnCredential, autoCreateVault bool) (*didtypes.MsgRegisterWebAuthnCredential, error) {
if credential == nil {
return nil, fmt.Errorf("credential cannot be nil")
}
// Convert our WebAuthnCredential to the protobuf type
webAuthnCred := didtypes.WebAuthnCredential{
CredentialId: credential.ID,
PublicKey: credential.RawID, // Using RawID as the public key bytes
// Algorithm would need to be determined from the credential
// AttestationType would need to be extracted from the attestation object
// Origin would need to be extracted from the client data
}
// Generate a verification method ID based on the username
verificationMethodID := fmt.Sprintf("did:sonr:%s#webauthn-1", username)
return &didtypes.MsgRegisterWebAuthnCredential{
Controller: controller,
Username: username,
WebauthnCredential: webAuthnCred,
VerificationMethodId: verificationMethodID,
AutoCreateVault: autoCreateVault,
}, nil
}
+33
View File
@@ -0,0 +1,33 @@
package did
import (
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"github.com/sonr-io/sonr/client/config"
)
// TestDIDClient tests DID module client functionality.
func TestDIDClient(t *testing.T) {
// Create mock connection
conn, _ := grpc.Dial("localhost:9090", grpc.WithInsecure())
cfg := config.LocalNetwork()
client := NewClient(conn, &cfg)
require.NotNil(t, client)
}
// TestBasicDIDFunctionality tests that we can create a client
func TestBasicDIDFunctionality(t *testing.T) {
// Just test that the package compiles and basic functions work
conn, _ := grpc.Dial("localhost:9090", grpc.WithInsecure())
cfg := config.LocalNetwork()
client := NewClient(conn, &cfg)
require.NotNil(t, client, "DID client should not be nil")
// Test that the client implements the Client interface
var _ Client = client
}
+613
View File
@@ -0,0 +1,613 @@
// Package dwn provides a client interface for interacting with the Sonr DWN (Decentralized Web Node) module.
package dwn
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
dwntypes "github.com/sonr-io/sonr/x/dwn/types"
)
// Client provides an interface for interacting with the DWN module.
type Client interface {
// Record Operations
CreateRecord(ctx context.Context, opts *CreateRecordOptions) (*Record, error)
ReadRecord(ctx context.Context, recordID string) (*Record, error)
UpdateRecord(ctx context.Context, recordID string, opts *UpdateRecordOptions) (*Record, error)
DeleteRecord(ctx context.Context, recordID string) error
// Query Operations
QueryRecords(ctx context.Context, query *RecordQuery) (*RecordQueryResponse, error)
ListRecords(ctx context.Context, opts *ListRecordsOptions) (*RecordListResponse, error)
// Permission Operations
GrantPermission(ctx context.Context, opts *GrantPermissionOptions) (*Permission, error)
RevokePermission(ctx context.Context, permissionID string) error
ListPermissions(ctx context.Context, opts *ListPermissionsOptions) (*PermissionListResponse, error)
// Protocol Operations
InstallProtocol(ctx context.Context, protocol *Protocol) error
UninstallProtocol(ctx context.Context, protocolURI string) error
ListProtocols(ctx context.Context) ([]*Protocol, error)
// Encryption Operations
EncryptRecord(ctx context.Context, recordID string, opts *EncryptionOptions) error
DecryptRecord(ctx context.Context, recordID string) (*DecryptedRecord, error)
// Vault Operations
CreateVault(ctx context.Context, opts *VaultOptions) (*Vault, error)
ListVaults(ctx context.Context) ([]*Vault, error)
ExportVault(ctx context.Context, vaultID string) (*VaultExport, error)
ImportVault(ctx context.Context, vaultData *VaultExport) (*Vault, error)
}
// Record represents a DWN record.
type Record struct {
ID string `json:"id"`
DID string `json:"did"`
SchemaURI string `json:"schema_uri,omitempty"`
ProtocolURI string `json:"protocol_uri,omitempty"`
ContextID string `json:"context_id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Data []byte `json:"data"`
Metadata map[string]any `json:"metadata,omitempty"`
Encrypted bool `json:"encrypted"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// CreateRecordOptions configures record creation.
type CreateRecordOptions struct {
DID string `json:"did"`
SchemaURI string `json:"schema_uri,omitempty"`
ProtocolURI string `json:"protocol_uri,omitempty"`
ContextID string `json:"context_id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Data []byte `json:"data"`
Metadata map[string]any `json:"metadata,omitempty"`
Encrypt bool `json:"encrypt,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// UpdateRecordOptions configures record updates.
type UpdateRecordOptions struct {
Data []byte `json:"data,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// RecordQuery defines query parameters for records.
type RecordQuery struct {
DID string `json:"did,omitempty"`
SchemaURI string `json:"schema_uri,omitempty"`
ProtocolURI string `json:"protocol_uri,omitempty"`
ContextID string `json:"context_id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
DateRange *DateRange `json:"date_range,omitempty"`
}
// DateRange specifies a date range for queries.
type DateRange struct {
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
}
// RecordQueryResponse contains query results.
type RecordQueryResponse struct {
Records []*Record `json:"records"`
TotalCount uint64 `json:"total_count"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// ListRecordsOptions configures record listing.
type ListRecordsOptions struct {
DID string `json:"did,omitempty"`
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
}
// RecordListResponse contains a list of records.
type RecordListResponse struct {
Records []*Record `json:"records"`
TotalCount uint64 `json:"total_count"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// Permission represents a DWN permission.
type Permission struct {
ID string `json:"id"`
Grantor string `json:"grantor"`
Grantee string `json:"grantee"`
Scope string `json:"scope"`
Actions []string `json:"actions"`
Conditions map[string]any `json:"conditions,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
CreatedAt string `json:"created_at"`
}
// GrantPermissionOptions configures permission granting.
type GrantPermissionOptions struct {
Grantee string `json:"grantee"`
Scope string `json:"scope"`
Actions []string `json:"actions"`
Conditions map[string]any `json:"conditions,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
}
// ListPermissionsOptions configures permission listing.
type ListPermissionsOptions struct {
Grantee string `json:"grantee,omitempty"`
Scope string `json:"scope,omitempty"`
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
}
// PermissionListResponse contains a list of permissions.
type PermissionListResponse struct {
Permissions []*Permission `json:"permissions"`
TotalCount uint64 `json:"total_count"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// Protocol represents a DWN protocol.
type Protocol struct {
URI string `json:"uri"`
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
Schema map[string]any `json:"schema"`
Rules map[string]any `json:"rules,omitempty"`
CreatedAt string `json:"created_at"`
}
// EncryptionOptions configures record encryption.
type EncryptionOptions struct {
Algorithm string `json:"algorithm,omitempty"`
Recipients []string `json:"recipients,omitempty"`
KeyDerivation map[string]any `json:"key_derivation,omitempty"`
}
// DecryptedRecord contains decrypted record data.
type DecryptedRecord struct {
Record *Record `json:"record"`
Data []byte `json:"data"`
Algorithm string `json:"algorithm"`
}
// Vault represents a DWN vault.
type Vault struct {
ID string `json:"id"`
DID string `json:"did"`
Name string `json:"name"`
Type string `json:"type"`
Config map[string]any `json:"config"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at,omitempty"`
}
// VaultOptions configures vault creation.
type VaultOptions struct {
DID string `json:"did"`
Name string `json:"name"`
Type string `json:"type,omitempty"`
Config map[string]any `json:"config,omitempty"`
}
// VaultExport contains exported vault data.
type VaultExport struct {
Vault *Vault `json:"vault"`
Records []*Record `json:"records"`
Protocols []*Protocol `json:"protocols"`
Metadata map[string]any `json:"metadata"`
}
// client implements the DWN Client interface.
type client struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
// Service clients for DWN module
queryClient dwntypes.QueryClient
msgClient dwntypes.MsgClient
txClient tx.ServiceClient
}
// NewClient creates a new DWN module client.
func NewClient(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) Client {
return &client{
grpcConn: grpcConn,
config: cfg,
queryClient: dwntypes.NewQueryClient(grpcConn),
msgClient: dwntypes.NewMsgClient(grpcConn),
txClient: tx.NewServiceClient(grpcConn),
}
}
// CreateRecord creates a new record in the DWN.
func (c *client) CreateRecord(ctx context.Context, opts *CreateRecordOptions) (*Record, error) {
// TODO: Implement record creation using DWN module
// Should build MsgRecordsWrite with proper descriptor
// Validate record data size and format
// Handle encryption if requested in options
// Submit transaction and return created record with ID
return nil, errors.NewModuleError("dwn", "CreateRecord",
fmt.Errorf("record creation not yet implemented"))
}
// ReadRecord retrieves a record by ID.
func (c *client) ReadRecord(ctx context.Context, recordID string) (*Record, error) {
// TODO: Implement record reading using DWN module query client
// Should query chain state for record by ID
// Check read permissions and UCAN authorization
// Decrypt record data if encrypted
// Return complete record with metadata
return nil, errors.NewModuleError("dwn", "ReadRecord",
fmt.Errorf("record reading not yet implemented"))
}
// UpdateRecord updates an existing record.
func (c *client) UpdateRecord(ctx context.Context, recordID string, opts *UpdateRecordOptions) (*Record, error) {
// TODO: Implement record updates using DWN module
// Should validate record ownership and update permissions
// Build MsgRecordsWrite with updated data
// Preserve original record metadata unless modified
// Handle encryption for updated data
return nil, errors.NewModuleError("dwn", "UpdateRecord",
fmt.Errorf("record updates not yet implemented"))
}
// DeleteRecord deletes a record.
func (c *client) DeleteRecord(ctx context.Context, recordID string) error {
// TODO: Implement record deletion using DWN module
// Should validate record ownership and delete permissions
// Build MsgRecordsDelete and submit to chain
// Handle soft delete vs hard delete based on protocol
// Clean up associated IPFS data if applicable
return errors.NewModuleError("dwn", "DeleteRecord",
fmt.Errorf("record deletion not yet implemented"))
}
// QueryRecords queries records based on specified criteria.
func (c *client) QueryRecords(ctx context.Context, query *RecordQuery) (*RecordQueryResponse, error) {
// TODO: Implement record querying using DWN module
// Should support complex queries with multiple filters
// Filter by DID, schema, protocol, context, parent
// Support date range and tag-based filtering
// Return paginated results with total count
return nil, errors.NewModuleError("dwn", "QueryRecords",
fmt.Errorf("record querying not yet implemented"))
}
// ListRecords lists records with pagination.
func (c *client) ListRecords(ctx context.Context, opts *ListRecordsOptions) (*RecordListResponse, error) {
// TODO: Implement record listing using DWN module
// Should support pagination with limit/offset
// Filter by DID if specified
// Return records with basic metadata
// Handle empty result sets gracefully
return nil, errors.NewModuleError("dwn", "ListRecords",
fmt.Errorf("record listing not yet implemented"))
}
// GrantPermission grants a permission to access records.
func (c *client) GrantPermission(ctx context.Context, opts *GrantPermissionOptions) (*Permission, error) {
// TODO: Implement permission granting using DWN module
// Should build MsgPermissionsGrant with proper conditions
// Validate grantee DID and permission scope
// Set expiration and action restrictions
// Return permission record with unique ID
return nil, errors.NewModuleError("dwn", "GrantPermission",
fmt.Errorf("permission granting not yet implemented"))
}
// RevokePermission revokes a previously granted permission.
func (c *client) RevokePermission(ctx context.Context, permissionID string) error {
// TODO: Implement permission revocation using DWN module
// Should build MsgPermissionsRevoke and submit to chain
// Validate permission ownership before revocation
// Update permission status to revoked
// Notify affected systems of permission changes
return errors.NewModuleError("dwn", "RevokePermission",
fmt.Errorf("permission revocation not yet implemented"))
}
// ListPermissions lists permissions with optional filtering.
func (c *client) ListPermissions(ctx context.Context, opts *ListPermissionsOptions) (*PermissionListResponse, error) {
// TODO: Implement permission listing using DWN module
// Should support filtering by grantee, scope, status
// Include permission expiration and condition info
// Support pagination with limit/offset
// Return permissions with grant metadata
return nil, errors.NewModuleError("dwn", "ListPermissions",
fmt.Errorf("permission listing not yet implemented"))
}
// InstallProtocol installs a new protocol.
func (c *client) InstallProtocol(ctx context.Context, protocol *Protocol) error {
// TODO: Implement protocol installation using DWN module
// Should build MsgProtocolsConfigure and submit to chain
// Validate protocol schema and rules format
// Check protocol URI uniqueness and versioning
// Store protocol definition for record validation
return errors.NewModuleError("dwn", "InstallProtocol",
fmt.Errorf("protocol installation not yet implemented"))
}
// UninstallProtocol uninstalls a protocol.
func (c *client) UninstallProtocol(ctx context.Context, protocolURI string) error {
// TODO: Implement protocol uninstallation using DWN module
// Should check for existing records using this protocol
// Prevent uninstallation if records depend on protocol
// Remove protocol definition from storage
// Handle graceful protocol deprecation
return errors.NewModuleError("dwn", "UninstallProtocol",
fmt.Errorf("protocol uninstallation not yet implemented"))
}
// ListProtocols lists installed protocols.
func (c *client) ListProtocols(ctx context.Context) ([]*Protocol, error) {
// TODO: Implement protocol listing using DWN module
// Should query chain state for installed protocols
// Return protocols with schema, rules, and version info
// Include protocol usage statistics if available
// Handle empty protocol list gracefully
return nil, errors.NewModuleError("dwn", "ListProtocols",
fmt.Errorf("protocol listing not yet implemented"))
}
// EncryptRecord encrypts a record.
func (c *client) EncryptRecord(ctx context.Context, recordID string, opts *EncryptionOptions) error {
// TODO: Implement record encryption using DWN module
// Should validate record ownership and encryption options
// Use AES-GCM with secure key derivation from recipients
// Store encrypted data with authentication tag
// Update record metadata to mark as encrypted
return errors.NewModuleError("dwn", "EncryptRecord",
fmt.Errorf("record encryption not yet implemented"))
}
// DecryptRecord decrypts a record.
func (c *client) DecryptRecord(ctx context.Context, recordID string) (*DecryptedRecord, error) {
// TODO: Implement record decryption using DWN module
// Should validate decryption permissions and key access
// Use stored encryption algorithm and key derivation
// Verify authentication tag before returning data
// Return decrypted record with original format info
return nil, errors.NewModuleError("dwn", "DecryptRecord",
fmt.Errorf("record decryption not yet implemented"))
}
// CreateVault creates a new vault.
func (c *client) CreateVault(ctx context.Context, opts *VaultOptions) (*Vault, error) {
// TODO: Implement vault creation using DWN module and Motor plugin
// Should validate DID ownership and vault configuration
// Use Motor WASM enclave for secure vault initialization
// Generate vault keys using hardware-backed security
// Store vault metadata on chain with IPFS references
return nil, errors.NewModuleError("dwn", "CreateVault",
fmt.Errorf("vault creation not yet implemented"))
}
// ListVaults lists available vaults.
func (c *client) ListVaults(ctx context.Context) ([]*Vault, error) {
// TODO: Implement vault listing using DWN module
// Should query chain state for user vaults
// Return vaults with metadata and configuration
// Include vault status and last update information
// Handle access permissions for vault visibility
return nil, errors.NewModuleError("dwn", "ListVaults",
fmt.Errorf("vault listing not yet implemented"))
}
// ExportVault exports vault data.
func (c *client) ExportVault(ctx context.Context, vaultID string) (*VaultExport, error) {
// TODO: Implement vault export using DWN module and Motor plugin
// Should validate vault ownership and export permissions
// Use Motor plugin to securely export vault contents
// Include all records, protocols, and permissions
// Encrypt export data for secure transfer
return nil, errors.NewModuleError("dwn", "ExportVault",
fmt.Errorf("vault export not yet implemented"))
}
// ImportVault imports vault data.
func (c *client) ImportVault(ctx context.Context, vaultData *VaultExport) (*Vault, error) {
// TODO: Implement vault import using DWN module and Motor plugin
// Should validate import data integrity and format
// Use Motor plugin to securely import vault contents
// Restore records, protocols, and permissions
// Handle conflicts with existing data gracefully
return nil, errors.NewModuleError("dwn", "ImportVault",
fmt.Errorf("vault import not yet implemented"))
}
// Utility functions
// GenerateRecordID generates a unique record ID.
func GenerateRecordID() string {
// TODO: Implement proper record ID generation
return fmt.Sprintf("record_%d", time.Now().UnixNano())
}
// ValidateRecordData validates record data.
func ValidateRecordData(data []byte) error {
if len(data) == 0 {
return fmt.Errorf("record data cannot be empty")
}
// Add size limits and other validation as needed
if len(data) > 10*1024*1024 { // 10MB limit
return fmt.Errorf("record data too large")
}
return nil
}
// CreateDefaultProtocol creates a default protocol configuration.
func CreateDefaultProtocol(name, version string) *Protocol {
return &Protocol{
URI: fmt.Sprintf("https://protocols.sonr.io/%s/%s", name, version),
Name: name,
Version: version,
Description: fmt.Sprintf("Default protocol for %s", name),
Schema: map[string]any{},
Rules: map[string]any{},
}
}
// Message Builders - These create the actual transaction messages
// BuildMsgRecordsWrite builds a MsgRecordsWrite message.
func BuildMsgRecordsWrite(author, target string, opts *CreateRecordOptions) (*dwntypes.MsgRecordsWrite, error) {
if opts == nil {
return nil, fmt.Errorf("options cannot be nil")
}
// Create message descriptor
descriptor := &dwntypes.DWNMessageDescriptor{
InterfaceName: "Records",
Method: "Write",
MessageTimestamp: time.Now().Format(time.RFC3339),
DataFormat: "application/json", // Default format
DataSize: int64(len(opts.Data)),
}
return &dwntypes.MsgRecordsWrite{
Author: author,
Target: target,
Descriptor_: descriptor,
Authorization: "", // Will be set by the transaction builder
Data: opts.Data,
}, nil
}
// BuildMsgRecordsDelete builds a MsgRecordsDelete message.
func BuildMsgRecordsDelete(author, target, recordID string) (*dwntypes.MsgRecordsDelete, error) {
if recordID == "" {
return nil, fmt.Errorf("record ID cannot be empty")
}
// Create message descriptor
descriptor := &dwntypes.DWNMessageDescriptor{
InterfaceName: "Records",
Method: "Delete",
MessageTimestamp: time.Now().Format(time.RFC3339),
}
return &dwntypes.MsgRecordsDelete{
Author: author,
Target: target,
RecordId: recordID,
Descriptor_: descriptor,
Authorization: "", // Will be set by the transaction builder
}, nil
}
// BuildMsgProtocolsConfigure builds a MsgProtocolsConfigure message.
func BuildMsgProtocolsConfigure(author, target string, protocol *Protocol) (*dwntypes.MsgProtocolsConfigure, error) {
if protocol == nil {
return nil, fmt.Errorf("protocol cannot be nil")
}
// Create message descriptor
descriptor := &dwntypes.DWNMessageDescriptor{
InterfaceName: "Protocols",
Method: "Configure",
MessageTimestamp: time.Now().Format(time.RFC3339),
}
// Note: The actual protocol definition would need to be serialized
// This is a placeholder implementation
return &dwntypes.MsgProtocolsConfigure{
Author: author,
Target: target,
Descriptor_: descriptor,
Authorization: "", // Will be set by the transaction builder
// Definition would be set here based on the protocol
}, nil
}
// BuildMsgPermissionsGrant builds a MsgPermissionsGrant message.
func BuildMsgPermissionsGrant(grantor, grantee, target string, grant *GrantPermissionOptions) (*dwntypes.MsgPermissionsGrant, error) {
if grant == nil {
return nil, fmt.Errorf("permission grant cannot be nil")
}
// Create message descriptor
descriptor := &dwntypes.DWNMessageDescriptor{
InterfaceName: "Permissions",
Method: "Grant",
MessageTimestamp: time.Now().Format(time.RFC3339),
}
// Note: The actual permission grant would need to be serialized
// This is a placeholder implementation
return &dwntypes.MsgPermissionsGrant{
Grantor: grantor,
Grantee: grantee,
Target: target,
Descriptor_: descriptor,
Authorization: "", // Will be set by the transaction builder
// PermissionGrant would be set here
}, nil
}
// BuildMsgPermissionsRevoke builds a MsgPermissionsRevoke message.
func BuildMsgPermissionsRevoke(grantor, permissionID string) (*dwntypes.MsgPermissionsRevoke, error) {
if permissionID == "" {
return nil, fmt.Errorf("permission ID cannot be empty")
}
// Create message descriptor
descriptor := &dwntypes.DWNMessageDescriptor{
InterfaceName: "Permissions",
Method: "Revoke",
MessageTimestamp: time.Now().Format(time.RFC3339),
}
return &dwntypes.MsgPermissionsRevoke{
Grantor: grantor,
PermissionId: permissionID,
Descriptor_: descriptor,
Authorization: "", // Will be set by the transaction builder
}, nil
}
// BuildMsgRotateVaultKeys builds a MsgRotateVaultKeys message.
func BuildMsgRotateVaultKeys(authority, vaultID string) *dwntypes.MsgRotateVaultKeys {
return &dwntypes.MsgRotateVaultKeys{
Authority: authority,
VaultId: vaultID,
}
}
+559
View File
@@ -0,0 +1,559 @@
// Package svc provides a client interface for interacting with the Sonr SVC (Service) module.
package svc
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
svctypes "github.com/sonr-io/sonr/x/svc/types"
)
// Client provides an interface for interacting with the SVC module.
type Client interface {
// Service Operations
RegisterService(ctx context.Context, opts *RegisterServiceOptions) (*Service, error)
UpdateService(ctx context.Context, serviceID string, opts *UpdateServiceOptions) (*Service, error)
DeregisterService(ctx context.Context, serviceID string) error
GetService(ctx context.Context, serviceID string) (*Service, error)
// Service Discovery
DiscoverServices(ctx context.Context, query *ServiceQuery) (*ServiceDiscoveryResponse, error)
ListServices(ctx context.Context, opts *ListServicesOptions) (*ServiceListResponse, error)
SearchServices(ctx context.Context, searchTerm string) (*ServiceSearchResponse, error)
// Domain Operations
RegisterDomain(ctx context.Context, opts *RegisterDomainOptions) (*Domain, error)
VerifyDomain(ctx context.Context, domain string) (*DomainVerification, error)
GetDomain(ctx context.Context, domain string) (*Domain, error)
ListDomains(ctx context.Context, opts *ListDomainsOptions) (*DomainListResponse, error)
// Service Capabilities
AddCapability(ctx context.Context, serviceID string, capability *Capability) error
RemoveCapability(ctx context.Context, serviceID string, capabilityID string) error
ListCapabilities(ctx context.Context, serviceID string) ([]*Capability, error)
// Service Endpoints
AddEndpoint(ctx context.Context, serviceID string, endpoint *Endpoint) error
UpdateEndpoint(ctx context.Context, serviceID string, endpointID string, opts *UpdateEndpointOptions) error
RemoveEndpoint(ctx context.Context, serviceID string, endpointID string) error
// Service Health
CheckServiceHealth(ctx context.Context, serviceID string) (*HealthStatus, error)
UpdateServiceHealth(ctx context.Context, serviceID string, status *HealthStatus) error
}
// Service represents a registered service.
type Service struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Owner string `json:"owner"`
Domain string `json:"domain,omitempty"`
Version string `json:"version"`
Type string `json:"type"`
Endpoints []*Endpoint `json:"endpoints"`
Capabilities []*Capability `json:"capabilities"`
Metadata map[string]any `json:"metadata,omitempty"`
HealthStatus *HealthStatus `json:"health_status,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// RegisterServiceOptions configures service registration.
type RegisterServiceOptions struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Domain string `json:"domain,omitempty"`
Version string `json:"version"`
Type string `json:"type"`
Endpoints []*Endpoint `json:"endpoints"`
Capabilities []*Capability `json:"capabilities,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// UpdateServiceOptions configures service updates.
type UpdateServiceOptions struct {
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// Endpoint represents a service endpoint.
type Endpoint struct {
ID string `json:"id"`
URL string `json:"url"`
Type string `json:"type"` // REST, GraphQL, gRPC, WebSocket, etc.
Method string `json:"method,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Enabled bool `json:"enabled"`
}
// UpdateEndpointOptions configures endpoint updates.
type UpdateEndpointOptions struct {
URL string `json:"url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
// Capability represents a service capability.
type Capability struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Type string `json:"type"`
Schema map[string]any `json:"schema,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Enabled bool `json:"enabled"`
}
// ServiceQuery defines query parameters for service discovery.
type ServiceQuery struct {
Type string `json:"type,omitempty"`
Domain string `json:"domain,omitempty"`
Tags []string `json:"tags,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
HealthStatus string `json:"health_status,omitempty"`
}
// ServiceDiscoveryResponse contains service discovery results.
type ServiceDiscoveryResponse struct {
Services []*Service `json:"services"`
TotalCount uint64 `json:"total_count"`
Query *ServiceQuery `json:"query"`
}
// ListServicesOptions configures service listing.
type ListServicesOptions struct {
Owner string `json:"owner,omitempty"`
Type string `json:"type,omitempty"`
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
}
// ServiceListResponse contains a list of services.
type ServiceListResponse struct {
Services []*Service `json:"services"`
TotalCount uint64 `json:"total_count"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// ServiceSearchResponse contains service search results.
type ServiceSearchResponse struct {
Services []*Service `json:"services"`
TotalCount uint64 `json:"total_count"`
SearchTerm string `json:"search_term"`
}
// Domain represents a registered domain.
type Domain struct {
Name string `json:"name"`
Owner string `json:"owner"`
Verified bool `json:"verified"`
Verification *DomainVerification `json:"verification,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
RegisteredAt string `json:"registered_at"`
ExpiresAt string `json:"expires_at,omitempty"`
}
// RegisterDomainOptions configures domain registration.
type RegisterDomainOptions struct {
Name string `json:"name"`
Metadata map[string]any `json:"metadata,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
}
// DomainVerification contains domain verification information.
type DomainVerification struct {
Method string `json:"method"` // DNS, HTTP, File
Token string `json:"token"` // Verification token
Challenge string `json:"challenge"` // Challenge string
Verified bool `json:"verified"`
VerifiedAt string `json:"verified_at,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
}
// ListDomainsOptions configures domain listing.
type ListDomainsOptions struct {
Owner string `json:"owner,omitempty"`
Verified *bool `json:"verified,omitempty"`
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
}
// DomainListResponse contains a list of domains.
type DomainListResponse struct {
Domains []*Domain `json:"domains"`
TotalCount uint64 `json:"total_count"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// HealthStatus represents service health status.
type HealthStatus struct {
Status string `json:"status"` // healthy, unhealthy, degraded, unknown
LastChecked string `json:"last_checked"`
Message string `json:"message,omitempty"`
Metrics map[string]any `json:"metrics,omitempty"`
Uptime string `json:"uptime,omitempty"`
}
// client implements the SVC Client interface.
type client struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
// Service clients for SVC module
queryClient svctypes.QueryClient
msgClient svctypes.MsgClient
txClient tx.ServiceClient
}
// NewClient creates a new SVC module client.
func NewClient(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) Client {
return &client{
grpcConn: grpcConn,
config: cfg,
queryClient: svctypes.NewQueryClient(grpcConn),
msgClient: svctypes.NewMsgClient(grpcConn),
txClient: tx.NewServiceClient(grpcConn),
}
}
// RegisterService registers a new service.
func (c *client) RegisterService(ctx context.Context, opts *RegisterServiceOptions) (*Service, error) {
// TODO: Implement service registration using SVC module
// Should build MsgRegisterService with proper validation
// Submit transaction to chain and wait for confirmation
// Handle domain verification if domain is provided
// Return complete service record with generated ID
return nil, errors.NewModuleError("svc", "RegisterService",
fmt.Errorf("service registration not yet implemented"))
}
// UpdateService updates an existing service.
func (c *client) UpdateService(ctx context.Context, serviceID string, opts *UpdateServiceOptions) (*Service, error) {
// TODO: Implement service updates using SVC module
// Should validate service ownership and permissions
// Build MsgUpdateService with selective field updates
// Preserve existing endpoints and capabilities unless modified
// Return updated service record
return nil, errors.NewModuleError("svc", "UpdateService",
fmt.Errorf("service updates not yet implemented"))
}
// DeregisterService deregisters a service.
func (c *client) DeregisterService(ctx context.Context, serviceID string) error {
// TODO: Implement service deregistration using SVC module
// Should validate service ownership before deregistration
// Build MsgDeregisterService and submit to chain
// Clean up associated domain registrations and capabilities
// Handle graceful shutdown of service endpoints
return errors.NewModuleError("svc", "DeregisterService",
fmt.Errorf("service deregistration not yet implemented"))
}
// GetService retrieves a service by ID.
func (c *client) GetService(ctx context.Context, serviceID string) (*Service, error) {
// TODO: Implement service retrieval using SVC query client
// Should query chain state for service record
// Convert protobuf service to client Service type
// Include current health status and endpoint information
// Handle service not found errors gracefully
return nil, errors.NewModuleError("svc", "GetService",
fmt.Errorf("service retrieval not yet implemented"))
}
// DiscoverServices discovers services based on query criteria.
func (c *client) DiscoverServices(ctx context.Context, query *ServiceQuery) (*ServiceDiscoveryResponse, error) {
// TODO: Implement service discovery using SVC module
// Should support filtering by type, domain, tags, capabilities
// Query chain state with proper pagination
// Filter results by health status if specified
// Return ranked results based on relevance
return nil, errors.NewModuleError("svc", "DiscoverServices",
fmt.Errorf("service discovery not yet implemented"))
}
// ListServices lists services with pagination.
func (c *client) ListServices(ctx context.Context, opts *ListServicesOptions) (*ServiceListResponse, error) {
// TODO: Implement service listing using SVC module
// Should support pagination with limit/offset
// Filter by owner and service type if specified
// Return services with basic metadata and status
// Handle empty result sets gracefully
return nil, errors.NewModuleError("svc", "ListServices",
fmt.Errorf("service listing not yet implemented"))
}
// SearchServices searches for services by term.
func (c *client) SearchServices(ctx context.Context, searchTerm string) (*ServiceSearchResponse, error) {
// TODO: Implement service search using SVC module
// Should search service names, descriptions, and tags
// Support fuzzy matching and relevance scoring
// Query multiple fields with OR logic
// Return results ranked by relevance
return nil, errors.NewModuleError("svc", "SearchServices",
fmt.Errorf("service search not yet implemented"))
}
// RegisterDomain registers a new domain.
func (c *client) RegisterDomain(ctx context.Context, opts *RegisterDomainOptions) (*Domain, error) {
// TODO: Implement domain registration using SVC module
// Should validate domain name format and availability
// Build MsgInitiateDomainVerification and submit to chain
// Generate verification challenge tokens
// Return domain record with verification instructions
return nil, errors.NewModuleError("svc", "RegisterDomain",
fmt.Errorf("domain registration not yet implemented"))
}
// VerifyDomain verifies domain ownership.
func (c *client) VerifyDomain(ctx context.Context, domain string) (*DomainVerification, error) {
// TODO: Implement domain verification using SVC module
// Should check DNS records, HTTP endpoints, or file verification
// Build MsgVerifyDomain and submit proof to chain
// Update domain status to verified upon success
// Handle verification failures with clear error messages
return nil, errors.NewModuleError("svc", "VerifyDomain",
fmt.Errorf("domain verification not yet implemented"))
}
// GetDomain retrieves domain information.
func (c *client) GetDomain(ctx context.Context, domain string) (*Domain, error) {
// TODO: Implement domain retrieval using SVC query client
// Should query chain state for domain registration
// Include verification status and expiration information
// Convert protobuf domain to client Domain type
// Handle domain not found cases
return nil, errors.NewModuleError("svc", "GetDomain",
fmt.Errorf("domain retrieval not yet implemented"))
}
// ListDomains lists domains with pagination.
func (c *client) ListDomains(ctx context.Context, opts *ListDomainsOptions) (*DomainListResponse, error) {
// TODO: Implement domain listing using SVC module
// Should support pagination and filtering by owner
// Filter by verification status if specified
// Return domains with metadata and expiration info
// Handle empty result sets
return nil, errors.NewModuleError("svc", "ListDomains",
fmt.Errorf("domain listing not yet implemented"))
}
// AddCapability adds a capability to a service.
func (c *client) AddCapability(ctx context.Context, serviceID string, capability *Capability) error {
// TODO: Implement capability addition using SVC module
// Should validate service ownership and capability schema
// Build MsgAddCapability and submit to chain
// Update service record with new capability
// Validate capability name uniqueness within service
return errors.NewModuleError("svc", "AddCapability",
fmt.Errorf("capability addition not yet implemented"))
}
// RemoveCapability removes a capability from a service.
func (c *client) RemoveCapability(ctx context.Context, serviceID string, capabilityID string) error {
// TODO: Implement capability removal using SVC module
// Should validate service ownership and capability existence
// Build MsgRemoveCapability and submit to chain
// Check for dependent services using this capability
// Handle cascading capability removal safely
return errors.NewModuleError("svc", "RemoveCapability",
fmt.Errorf("capability removal not yet implemented"))
}
// ListCapabilities lists service capabilities.
func (c *client) ListCapabilities(ctx context.Context, serviceID string) ([]*Capability, error) {
// TODO: Implement capability listing using SVC module
// Should query service record for capabilities
// Return capabilities with schemas and metadata
// Include capability status (enabled/disabled)
// Handle service not found errors
return nil, errors.NewModuleError("svc", "ListCapabilities",
fmt.Errorf("capability listing not yet implemented"))
}
// AddEndpoint adds an endpoint to a service.
func (c *client) AddEndpoint(ctx context.Context, serviceID string, endpoint *Endpoint) error {
// TODO: Implement endpoint addition using SVC module
// Should validate service ownership and endpoint URL format
// Build MsgAddEndpoint and submit to chain
// Validate endpoint accessibility if enabled
// Update service record with new endpoint
return errors.NewModuleError("svc", "AddEndpoint",
fmt.Errorf("endpoint addition not yet implemented"))
}
// UpdateEndpoint updates a service endpoint.
func (c *client) UpdateEndpoint(ctx context.Context, serviceID string, endpointID string, opts *UpdateEndpointOptions) error {
// TODO: Implement endpoint updates using SVC module
// Should validate service ownership and endpoint existence
// Build MsgUpdateEndpoint with selective field updates
// Validate new URL format and accessibility
// Preserve existing headers and metadata unless modified
return errors.NewModuleError("svc", "UpdateEndpoint",
fmt.Errorf("endpoint updates not yet implemented"))
}
// RemoveEndpoint removes an endpoint from a service.
func (c *client) RemoveEndpoint(ctx context.Context, serviceID string, endpointID string) error {
// TODO: Implement endpoint removal using SVC module
// Should validate service ownership and endpoint existence
// Build MsgRemoveEndpoint and submit to chain
// Check if endpoint is primary before removal
// Handle graceful endpoint shutdown
return errors.NewModuleError("svc", "RemoveEndpoint",
fmt.Errorf("endpoint removal not yet implemented"))
}
// CheckServiceHealth checks the health status of a service.
func (c *client) CheckServiceHealth(ctx context.Context, serviceID string) (*HealthStatus, error) {
// TODO: Implement health checking using SVC module
// Should query service endpoints for health status
// Aggregate health across multiple endpoints
// Check endpoint response times and error rates
// Return comprehensive health report with metrics
return nil, errors.NewModuleError("svc", "CheckServiceHealth",
fmt.Errorf("health checking not yet implemented"))
}
// UpdateServiceHealth updates the health status of a service.
func (c *client) UpdateServiceHealth(ctx context.Context, serviceID string, status *HealthStatus) error {
// TODO: Implement health status updates using SVC module
// Should validate service ownership and status format
// Build MsgUpdateServiceHealth and submit to chain
// Update service discovery with new health status
// Store health metrics and historical data
return errors.NewModuleError("svc", "UpdateServiceHealth",
fmt.Errorf("health status updates not yet implemented"))
}
// Utility functions
// GenerateServiceID generates a unique service ID.
func GenerateServiceID(name string) string {
// TODO: Implement proper service ID generation
return fmt.Sprintf("svc_%s_%d", name, time.Now().UnixNano())
}
// ValidateServiceName validates a service name.
func ValidateServiceName(name string) error {
if len(name) == 0 {
return fmt.Errorf("service name cannot be empty")
}
if len(name) > 100 {
return fmt.Errorf("service name too long")
}
return nil
}
// CreateDefaultEndpoint creates a default HTTP endpoint.
func CreateDefaultEndpoint(url string) *Endpoint {
return &Endpoint{
ID: fmt.Sprintf("endpoint_%d", time.Now().UnixNano()),
URL: url,
Type: "REST",
Method: "GET",
Enabled: true,
}
}
// CreateHealthyStatus creates a healthy status.
func CreateHealthyStatus() *HealthStatus {
return &HealthStatus{
Status: "healthy",
LastChecked: time.Now().UTC().Format(time.RFC3339),
Message: "Service is operating normally",
}
}
// Message Builders - These create the actual transaction messages
// BuildMsgRegisterService builds a MsgRegisterService message.
func BuildMsgRegisterService(creator string, opts *RegisterServiceOptions) (*svctypes.MsgRegisterService, error) {
if opts == nil {
return nil, fmt.Errorf("options cannot be nil")
}
if err := ValidateServiceName(opts.Name); err != nil {
return nil, fmt.Errorf("invalid service name: %w", err)
}
// Generate service ID if not provided
serviceID := GenerateServiceID(opts.Name)
// Extract requested permissions from capabilities
var requestedPermissions []string
for _, cap := range opts.Capabilities {
if cap != nil {
requestedPermissions = append(requestedPermissions, cap.Name)
}
}
return &svctypes.MsgRegisterService{
Creator: creator,
ServiceId: serviceID,
Domain: opts.Domain,
RequestedPermissions: requestedPermissions,
UcanDelegationChain: "", // Will be set if UCAN is used
}, nil
}
// BuildMsgInitiateDomainVerification builds a MsgInitiateDomainVerification message.
func BuildMsgInitiateDomainVerification(creator, domain string) (*svctypes.MsgInitiateDomainVerification, error) {
if domain == "" {
return nil, fmt.Errorf("domain cannot be empty")
}
return &svctypes.MsgInitiateDomainVerification{
Creator: creator,
Domain: domain,
}, nil
}
// BuildMsgVerifyDomain builds a MsgVerifyDomain message.
func BuildMsgVerifyDomain(creator, domain string) (*svctypes.MsgVerifyDomain, error) {
if domain == "" {
return nil, fmt.Errorf("domain cannot be empty")
}
return &svctypes.MsgVerifyDomain{
Creator: creator,
Domain: domain,
}, nil
}
+532
View File
@@ -0,0 +1,532 @@
// Package ucan provides a client interface for interacting with UCAN (User-Controlled Authorization Networks) functionality.
package ucan
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/keys"
)
// Client provides an interface for UCAN operations.
type Client interface {
// UCAN Token Operations
CreateToken(ctx context.Context, req *CreateTokenRequest) (*UCANToken, error)
AttenuateToken(ctx context.Context, req *AttenuateTokenRequest) (*UCANToken, error)
ValidateToken(ctx context.Context, token string) (*TokenValidation, error)
RevokeToken(ctx context.Context, tokenID string) error
// Capability Operations
CreateCapability(ctx context.Context, req *CreateCapabilityRequest) (*Capability, error)
ListCapabilities(ctx context.Context, opts *ListCapabilitiesOptions) (*CapabilityListResponse, error)
RevokeCapability(ctx context.Context, capabilityID string) error
// Delegation Operations
CreateDelegation(ctx context.Context, req *CreateDelegationRequest) (*Delegation, error)
ListDelegations(ctx context.Context, opts *ListDelegationsOptions) (*DelegationListResponse, error)
RevokeDelegation(ctx context.Context, delegationID string) error
// Verification Operations
VerifyToken(ctx context.Context, token string) (*VerificationResult, error)
VerifyCapability(ctx context.Context, token string, resource string, action string) (*CapabilityVerification, error)
// Chain Operations
ValidateTokenChain(ctx context.Context, tokenChain []string) (*ChainValidation, error)
ResolveTokenChain(ctx context.Context, token string) (*TokenChain, error)
}
// UCANToken represents a UCAN JWT token.
type UCANToken struct {
Token string `json:"token"` // JWT string
ID string `json:"id"` // Token ID
Issuer string `json:"issuer"` // Issuer DID
Audience string `json:"audience"` // Audience DID
Subject string `json:"subject,omitempty"` // Subject DID
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
NotBefore time.Time `json:"not_before,omitempty"`
Facts []string `json:"facts,omitempty"`
Capabilities []*Capability `json:"capabilities"`
Proof *Proof `json:"proof"`
Metadata map[string]any `json:"metadata,omitempty"`
}
// CreateTokenRequest configures UCAN token creation.
type CreateTokenRequest struct {
Audience string `json:"audience"` // Target audience DID
Subject string `json:"subject,omitempty"` // Subject DID (if different from issuer)
Capabilities []*Capability `json:"capabilities"` // Granted capabilities
Facts []string `json:"facts,omitempty"` // Additional facts
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Expiration time
NotBefore *time.Time `json:"not_before,omitempty"` // Validity start time
Metadata map[string]any `json:"metadata,omitempty"` // Additional metadata
}
// AttenuateTokenRequest configures token attenuation.
type AttenuateTokenRequest struct {
ParentToken string `json:"parent_token"` // Parent token to attenuate
Audience string `json:"audience"` // New audience DID
Capabilities []*Capability `json:"capabilities"` // Attenuated capabilities
Facts []string `json:"facts,omitempty"` // Additional facts
ExpiresAt *time.Time `json:"expires_at,omitempty"` // New expiration (must be earlier)
Metadata map[string]any `json:"metadata,omitempty"` // Additional metadata
}
// Capability represents a UCAN capability.
type Capability struct {
Resource string `json:"resource"` // Resource URI
Actions []string `json:"actions"` // Allowed actions
Conditions map[string]any `json:"conditions,omitempty"` // Capability conditions
Caveats []*Caveat `json:"caveats,omitempty"` // Additional restrictions
}
// Caveat represents a capability caveat (restriction).
type Caveat struct {
Type string `json:"type"` // Caveat type
Condition map[string]any `json:"condition"` // Caveat condition
}
// Proof represents cryptographic proof of authority.
type Proof struct {
Type string `json:"type"` // Proof type (e.g., "Ed25519", "ECDSA")
Created string `json:"created"` // Proof creation time
Signature string `json:"signature"` // Cryptographic signature
Challenge string `json:"challenge,omitempty"` // Challenge if required
}
// TokenValidation contains token validation results.
type TokenValidation struct {
Valid bool `json:"valid"`
Token *UCANToken `json:"token,omitempty"`
Errors []string `json:"errors,omitempty"`
Warnings []string `json:"warnings,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
Chain []*UCANToken `json:"chain,omitempty"`
}
// CreateCapabilityRequest configures capability creation.
type CreateCapabilityRequest struct {
Resource string `json:"resource"`
Actions []string `json:"actions"`
Conditions map[string]any `json:"conditions,omitempty"`
Caveats []*Caveat `json:"caveats,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// ListCapabilitiesOptions configures capability listing.
type ListCapabilitiesOptions struct {
Resource string `json:"resource,omitempty"`
Action string `json:"action,omitempty"`
Owner string `json:"owner,omitempty"`
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
}
// CapabilityListResponse contains a list of capabilities.
type CapabilityListResponse struct {
Capabilities []*Capability `json:"capabilities"`
TotalCount uint64 `json:"total_count"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// Delegation represents a UCAN delegation.
type Delegation struct {
ID string `json:"id"`
From string `json:"from"` // Delegator DID
To string `json:"to"` // Delegatee DID
Token *UCANToken `json:"token"` // Delegation token
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
}
// CreateDelegationRequest configures delegation creation.
type CreateDelegationRequest struct {
To string `json:"to"` // Delegatee DID
Capabilities []*Capability `json:"capabilities"` // Delegated capabilities
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Delegation expiration
Facts []string `json:"facts,omitempty"` // Additional facts
Metadata map[string]any `json:"metadata,omitempty"` // Additional metadata
}
// ListDelegationsOptions configures delegation listing.
type ListDelegationsOptions struct {
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Active *bool `json:"active,omitempty"` // Filter by active status
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset,omitempty"`
}
// DelegationListResponse contains a list of delegations.
type DelegationListResponse struct {
Delegations []*Delegation `json:"delegations"`
TotalCount uint64 `json:"total_count"`
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
}
// VerificationResult contains token verification results.
type VerificationResult struct {
Valid bool `json:"valid"`
Token *UCANToken `json:"token,omitempty"`
Chain []*UCANToken `json:"chain,omitempty"`
Errors []string `json:"errors,omitempty"`
Capabilities []*Capability `json:"capabilities,omitempty"`
}
// CapabilityVerification contains capability verification results.
type CapabilityVerification struct {
Authorized bool `json:"authorized"`
Capability *Capability `json:"capability,omitempty"`
Token *UCANToken `json:"token,omitempty"`
Reason string `json:"reason,omitempty"`
Conditions []string `json:"conditions,omitempty"`
}
// ChainValidation contains token chain validation results.
type ChainValidation struct {
Valid bool `json:"valid"`
Chain []*UCANToken `json:"chain"`
Errors []string `json:"errors,omitempty"`
Root *UCANToken `json:"root,omitempty"`
}
// TokenChain represents a resolved token chain.
type TokenChain struct {
Token *UCANToken `json:"token"`
Parents []*UCANToken `json:"parents"`
Root *UCANToken `json:"root"`
Depth int `json:"depth"`
Valid bool `json:"valid"`
Errors []string `json:"errors,omitempty"`
}
// client implements the UCAN Client interface.
type client struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
keyring keys.KeyringManager
// UCAN operations are primarily handled through the DWN plugin
// and don't require separate gRPC clients
}
// NewClient creates a new UCAN client.
func NewClient(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) Client {
return &client{
grpcConn: grpcConn,
config: cfg,
// keyring will be injected when needed
}
}
// WithKeyring sets the keyring for UCAN operations.
func (c *client) WithKeyring(keyring keys.KeyringManager) Client {
c.keyring = keyring
return c
}
// CreateToken creates a new UCAN token.
func (c *client) CreateToken(ctx context.Context, req *CreateTokenRequest) (*UCANToken, error) {
if c.keyring == nil {
return nil, fmt.Errorf("keyring required for token creation")
}
// Convert request to keyring format
ucanReq := &keys.UCANRequest{
AudienceDID: req.Audience,
Capabilities: capabilitiesToMap(req.Capabilities),
Facts: req.Facts,
NotBefore: req.NotBefore,
ExpiresAt: req.ExpiresAt,
}
// Create token using keyring (DWN plugin)
token, err := c.keyring.CreateOriginToken(ctx, ucanReq)
if err != nil {
return nil, errors.NewModuleError("ucan", "CreateToken", err)
}
// Convert to our format
return convertToUCANToken(token, req), nil
}
// AttenuateToken creates an attenuated UCAN token.
func (c *client) AttenuateToken(ctx context.Context, req *AttenuateTokenRequest) (*UCANToken, error) {
if c.keyring == nil {
return nil, fmt.Errorf("keyring required for token attenuation")
}
// Convert request to keyring format
attenuateReq := &keys.AttenuatedUCANRequest{
ParentToken: req.ParentToken,
AudienceDID: req.Audience,
Capabilities: capabilitiesToMap(req.Capabilities),
Facts: req.Facts,
ExpiresAt: req.ExpiresAt,
}
// Create attenuated token using keyring
token, err := c.keyring.CreateAttenuatedToken(ctx, attenuateReq)
if err != nil {
return nil, errors.NewModuleError("ucan", "AttenuateToken", err)
}
// Convert to our format
return convertToUCANToken(token, nil), nil
}
// ValidateToken validates a UCAN token.
func (c *client) ValidateToken(ctx context.Context, token string) (*TokenValidation, error) {
// TODO: Implement UCAN token validation using internal/ucan package
// Should parse JWT, validate signature, check expiration, verify capability chain
// Use ucan.ValidateToken() to perform cryptographic verification
// Return structured validation results with errors and warnings
return nil, errors.NewModuleError("ucan", "ValidateToken",
fmt.Errorf("token validation not yet implemented"))
}
// RevokeToken revokes a UCAN token.
func (c *client) RevokeToken(ctx context.Context, tokenID string) error {
// TODO: Implement UCAN token revocation mechanism
// Should add token to on-chain revocation list or registry
// Integrate with DWN module to store revocation records
// Notify dependent systems of token revocation
return errors.NewModuleError("ucan", "RevokeToken",
fmt.Errorf("token revocation not yet implemented"))
}
// CreateCapability creates a new capability.
func (c *client) CreateCapability(ctx context.Context, req *CreateCapabilityRequest) (*Capability, error) {
// TODO: Implement capability creation with proper validation
// Should validate resource URIs and action permissions
// Create capability following UCAN spec format
// Store capability in persistent storage for later use
return nil, errors.NewModuleError("ucan", "CreateCapability",
fmt.Errorf("capability creation not yet implemented"))
}
// ListCapabilities lists capabilities with filtering.
func (c *client) ListCapabilities(ctx context.Context, opts *ListCapabilitiesOptions) (*CapabilityListResponse, error) {
// TODO: Implement capability listing with filtering and pagination
// Should query stored capabilities by resource, action, owner
// Support pagination with limit/offset
// Return capabilities with metadata and expiration info
return nil, errors.NewModuleError("ucan", "ListCapabilities",
fmt.Errorf("capability listing not yet implemented"))
}
// RevokeCapability revokes a capability.
func (c *client) RevokeCapability(ctx context.Context, capabilityID string) error {
// TODO: Implement capability revocation mechanism
// Should invalidate capability and update revocation registry
// Cascade revocation to dependent capabilities
// Notify systems using the revoked capability
return errors.NewModuleError("ucan", "RevokeCapability",
fmt.Errorf("capability revocation not yet implemented"))
}
// CreateDelegation creates a new delegation.
func (c *client) CreateDelegation(ctx context.Context, req *CreateDelegationRequest) (*Delegation, error) {
// Delegation is essentially creating an attenuated token for someone else
attenuateReq := &AttenuateTokenRequest{
Audience: req.To,
Capabilities: req.Capabilities,
Facts: req.Facts,
ExpiresAt: req.ExpiresAt,
}
token, err := c.AttenuateToken(ctx, attenuateReq)
if err != nil {
return nil, errors.NewModuleError("ucan", "CreateDelegation", err)
}
// Convert to delegation format
delegation := &Delegation{
ID: fmt.Sprintf("delegation_%d", time.Now().UnixNano()),
From: token.Issuer,
To: req.To,
Token: token,
CreatedAt: token.IssuedAt,
ExpiresAt: token.ExpiresAt,
Revoked: false,
}
return delegation, nil
}
// ListDelegations lists delegations with filtering.
func (c *client) ListDelegations(ctx context.Context, opts *ListDelegationsOptions) (*DelegationListResponse, error) {
// TODO: Implement delegation listing with filtering
// Should query delegations by grantor, grantee, active status
// Support pagination and date range filtering
// Include delegation status and expiration information
return nil, errors.NewModuleError("ucan", "ListDelegations",
fmt.Errorf("delegation listing not yet implemented"))
}
// RevokeDelegation revokes a delegation.
func (c *client) RevokeDelegation(ctx context.Context, delegationID string) error {
// TODO: Implement delegation revocation mechanism
// Should revoke underlying UCAN token for delegation
// Update delegation status in storage
// Notify grantee of delegation revocation
return errors.NewModuleError("ucan", "RevokeDelegation",
fmt.Errorf("delegation revocation not yet implemented"))
}
// VerifyToken verifies a UCAN token and its chain.
func (c *client) VerifyToken(ctx context.Context, token string) (*VerificationResult, error) {
// TODO: Implement comprehensive UCAN token verification
// Should verify entire delegation chain from root to current token
// Check cryptographic signatures and capability bounds
// Validate against revocation lists and expiration times
// Use internal/ucan verification functions
return nil, errors.NewModuleError("ucan", "VerifyToken",
fmt.Errorf("token verification not yet implemented"))
}
// VerifyCapability verifies if a token grants access to a specific resource/action.
func (c *client) VerifyCapability(ctx context.Context, token string, resource string, action string) (*CapabilityVerification, error) {
// TODO: Implement capability-specific verification
// Should check if token contains capability for resource and action
// Verify capability conditions and caveats are satisfied
// Check resource URI patterns and action permissions
// Return detailed authorization result with reasoning
return nil, errors.NewModuleError("ucan", "VerifyCapability",
fmt.Errorf("capability verification not yet implemented"))
}
// ValidateTokenChain validates a chain of UCAN tokens.
func (c *client) ValidateTokenChain(ctx context.Context, tokenChain []string) (*ChainValidation, error) {
// TODO: Implement UCAN delegation chain validation
// Should verify each token in chain is properly attenuated
// Check parent-child relationships and capability inheritance
// Validate chronological order and expiration bounds
// Ensure no capability escalation in delegation chain
return nil, errors.NewModuleError("ucan", "ValidateTokenChain",
fmt.Errorf("token chain validation not yet implemented"))
}
// ResolveTokenChain resolves the full chain for a token.
func (c *client) ResolveTokenChain(ctx context.Context, token string) (*TokenChain, error) {
// TODO: Implement UCAN delegation chain resolution
// Should trace token back to root authority
// Build complete chain with parent tokens and proofs
// Resolve delegator DIDs and verify signatures
// Return structured chain with validation status
return nil, errors.NewModuleError("ucan", "ResolveTokenChain",
fmt.Errorf("token chain resolution not yet implemented"))
}
// Utility functions
// capabilitiesToMap converts capabilities to map format for keyring.
func capabilitiesToMap(capabilities []*Capability) []map[string]any {
var result []map[string]any
for _, cap := range capabilities {
capMap := map[string]any{
"can": cap.Actions,
"with": cap.Resource,
}
if len(cap.Conditions) > 0 {
capMap["conditions"] = cap.Conditions
}
if len(cap.Caveats) > 0 {
capMap["caveats"] = cap.Caveats
}
result = append(result, capMap)
}
return result
}
// convertToUCANToken converts keyring token to UCAN token format.
func convertToUCANToken(token *keys.UCANToken, req *CreateTokenRequest) *UCANToken {
ucanToken := &UCANToken{
Token: token.Token,
ID: fmt.Sprintf("ucan_%d", time.Now().UnixNano()),
Issuer: token.Issuer,
IssuedAt: time.Now(),
}
if req != nil {
ucanToken.Audience = req.Audience
ucanToken.Subject = req.Subject
ucanToken.Facts = req.Facts
ucanToken.Capabilities = req.Capabilities
ucanToken.Metadata = req.Metadata
if req.ExpiresAt != nil {
ucanToken.ExpiresAt = *req.ExpiresAt
} else {
ucanToken.ExpiresAt = time.Now().Add(time.Hour) // Default 1 hour
}
if req.NotBefore != nil {
ucanToken.NotBefore = *req.NotBefore
}
}
return ucanToken
}
// CreateDefaultCapability creates a basic capability.
func CreateDefaultCapability(resource string, actions []string) *Capability {
return &Capability{
Resource: resource,
Actions: actions,
}
}
// CreateVaultCapability creates a capability for vault operations.
func CreateVaultCapability(vaultID string) *Capability {
return &Capability{
Resource: fmt.Sprintf("vault://%s", vaultID),
Actions: []string{"read", "write", "sign", "export"},
}
}
// CreateServiceCapability creates a capability for service operations.
func CreateServiceCapability(serviceID string, actions []string) *Capability {
return &Capability{
Resource: fmt.Sprintf("service://%s", serviceID),
Actions: actions,
}
}
// ValidateCapability validates a capability structure.
func ValidateCapability(cap *Capability) error {
if cap.Resource == "" {
return fmt.Errorf("capability resource cannot be empty")
}
if len(cap.Actions) == 0 {
return fmt.Errorf("capability must have at least one action")
}
return nil
}
+354
View File
@@ -0,0 +1,354 @@
// Package query provides query functionality for reading blockchain state.
package query
import (
"context"
"fmt"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/types/query"
authTypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
)
// QueryClient provides an interface for querying blockchain state.
type QueryClient interface {
// Chain information
ChainInfo(ctx context.Context) (*ChainInfo, error)
NodeInfo(ctx context.Context) (*NodeInfo, error)
// Account queries
Account(ctx context.Context, address string) (*authTypes.QueryAccountResponse, error)
Accounts(ctx context.Context, pagination *PageRequest) (*authTypes.QueryAccountsResponse, error)
// Balance queries
Balance(ctx context.Context, address, denom string) (*banktypes.QueryBalanceResponse, error)
AllBalances(ctx context.Context, address string, pagination *PageRequest) (*banktypes.QueryAllBalancesResponse, error)
TotalSupply(ctx context.Context, pagination *PageRequest) (*banktypes.QueryTotalSupplyResponse, error)
SupplyOf(ctx context.Context, denom string) (*banktypes.QuerySupplyOfResponse, error)
// Staking queries
Validators(ctx context.Context, status string, pagination *PageRequest) (*stakingtypes.QueryValidatorsResponse, error)
Validator(ctx context.Context, validatorAddr string) (*stakingtypes.QueryValidatorResponse, error)
Delegations(ctx context.Context, delegatorAddr string, pagination *PageRequest) (*stakingtypes.QueryDelegatorDelegationsResponse, error)
Delegation(ctx context.Context, delegatorAddr, validatorAddr string) (*stakingtypes.QueryDelegationResponse, error)
// Transaction queries
Tx(ctx context.Context, hash string) (*TxResponse, error)
TxsByEvents(ctx context.Context, events []string, pagination *PageRequest) (*TxSearchResponse, error)
// Module-specific queries will be handled by module clients
}
// ChainInfo contains basic information about the blockchain.
type ChainInfo struct {
ChainID string `json:"chain_id"`
BlockHeight int64 `json:"block_height"`
BlockTime string `json:"block_time"`
NodeVersion string `json:"node_version"`
ApplicationVersion string `json:"application_version"`
}
// NodeInfo contains information about the connected node.
type NodeInfo struct {
NodeID string `json:"node_id"`
Network string `json:"network"`
Version string `json:"version"`
ListenAddr string `json:"listen_addr"`
Moniker string `json:"moniker"`
}
// TxResponse represents a transaction response.
type TxResponse struct {
Hash string `json:"hash"`
Height int64 `json:"height"`
Code uint32 `json:"code"`
Log string `json:"log"`
GasWanted int64 `json:"gas_wanted"`
GasUsed int64 `json:"gas_used"`
Events []Event `json:"events"`
}
// Event represents a transaction event.
type Event struct {
Type string `json:"type"`
Attributes []Attribute `json:"attributes"`
}
// Attribute represents an event attribute.
type Attribute struct {
Key string `json:"key"`
Value string `json:"value"`
}
// TxSearchResponse represents a transaction search response.
type TxSearchResponse struct {
Txs []*TxResponse `json:"txs"`
TotalCount int64 `json:"total_count"`
}
// PageRequest represents pagination parameters.
type PageRequest struct {
Key []byte `json:"key,omitempty"`
Offset uint64 `json:"offset,omitempty"`
Limit uint64 `json:"limit,omitempty"`
CountTotal bool `json:"count_total,omitempty"`
Reverse bool `json:"reverse,omitempty"`
}
// queryClient implements QueryClient.
type queryClient struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
// Cosmos SDK service clients
authQueryClient authTypes.QueryClient
bankQueryClient banktypes.QueryClient
stakingQueryClient stakingtypes.QueryClient
}
// NewQueryClient creates a new query client.
func NewQueryClient(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) (QueryClient, error) {
if grpcConn == nil {
return nil, fmt.Errorf("gRPC connection is required")
}
if cfg == nil {
return nil, fmt.Errorf("network configuration is required")
}
return &queryClient{
grpcConn: grpcConn,
config: cfg,
authQueryClient: authTypes.NewQueryClient(grpcConn),
bankQueryClient: banktypes.NewQueryClient(grpcConn),
stakingQueryClient: stakingtypes.NewQueryClient(grpcConn),
}, nil
}
// ChainInfo retrieves basic chain information.
func (qc *queryClient) ChainInfo(ctx context.Context) (*ChainInfo, error) {
// TODO: Implement proper chain info query using Tendermint RPC client
// Should query /status endpoint for current block height and time
// Get node version and application version from /abci_info
// Return comprehensive chain information with real-time data
return &ChainInfo{
ChainID: qc.config.ChainID,
NodeVersion: "unknown",
ApplicationVersion: "unknown",
}, nil
}
// NodeInfo retrieves node information.
func (qc *queryClient) NodeInfo(ctx context.Context) (*NodeInfo, error) {
// TODO: Implement proper node info query using Tendermint RPC client
// Should query /status endpoint for node ID and network info
// Get listen address and moniker from node status
// Include peer count and sync status information
return &NodeInfo{
NodeID: "unknown",
Network: qc.config.ChainID,
Version: "unknown",
}, nil
}
// Account retrieves account information by address.
func (qc *queryClient) Account(ctx context.Context, address string) (*authTypes.QueryAccountResponse, error) {
req := &authTypes.QueryAccountRequest{Address: address}
resp, err := qc.authQueryClient.Account(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query account %s", address)
}
return resp, nil
}
// Accounts retrieves all accounts with pagination.
func (qc *queryClient) Accounts(ctx context.Context, pagination *PageRequest) (*authTypes.QueryAccountsResponse, error) {
req := &authTypes.QueryAccountsRequest{}
if pagination != nil {
req.Pagination = &query.PageRequest{
Key: pagination.Key,
Offset: pagination.Offset,
Limit: pagination.Limit,
CountTotal: pagination.CountTotal,
Reverse: pagination.Reverse,
}
}
resp, err := qc.authQueryClient.Accounts(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query accounts")
}
return resp, nil
}
// Balance retrieves the balance of a specific denomination for an address.
func (qc *queryClient) Balance(ctx context.Context, address, denom string) (*banktypes.QueryBalanceResponse, error) {
req := &banktypes.QueryBalanceRequest{
Address: address,
Denom: denom,
}
resp, err := qc.bankQueryClient.Balance(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query balance for %s", address)
}
return resp, nil
}
// AllBalances retrieves all balances for an address.
func (qc *queryClient) AllBalances(ctx context.Context, address string, pagination *PageRequest) (*banktypes.QueryAllBalancesResponse, error) {
req := &banktypes.QueryAllBalancesRequest{Address: address}
if pagination != nil {
req.Pagination = &query.PageRequest{
Key: pagination.Key,
Offset: pagination.Offset,
Limit: pagination.Limit,
CountTotal: pagination.CountTotal,
Reverse: pagination.Reverse,
}
}
resp, err := qc.bankQueryClient.AllBalances(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query all balances for %s", address)
}
return resp, nil
}
// TotalSupply retrieves the total supply of all denominations.
func (qc *queryClient) TotalSupply(ctx context.Context, pagination *PageRequest) (*banktypes.QueryTotalSupplyResponse, error) {
req := &banktypes.QueryTotalSupplyRequest{}
if pagination != nil {
req.Pagination = &query.PageRequest{
Key: pagination.Key,
Offset: pagination.Offset,
Limit: pagination.Limit,
CountTotal: pagination.CountTotal,
Reverse: pagination.Reverse,
}
}
resp, err := qc.bankQueryClient.TotalSupply(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query total supply")
}
return resp, nil
}
// SupplyOf retrieves the supply of a specific denomination.
func (qc *queryClient) SupplyOf(ctx context.Context, denom string) (*banktypes.QuerySupplyOfResponse, error) {
req := &banktypes.QuerySupplyOfRequest{Denom: denom}
resp, err := qc.bankQueryClient.SupplyOf(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query supply of %s", denom)
}
return resp, nil
}
// Validators retrieves validators with optional status filter.
func (qc *queryClient) Validators(ctx context.Context, status string, pagination *PageRequest) (*stakingtypes.QueryValidatorsResponse, error) {
req := &stakingtypes.QueryValidatorsRequest{Status: status}
if pagination != nil {
req.Pagination = &query.PageRequest{
Key: pagination.Key,
Offset: pagination.Offset,
Limit: pagination.Limit,
CountTotal: pagination.CountTotal,
Reverse: pagination.Reverse,
}
}
resp, err := qc.stakingQueryClient.Validators(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query validators")
}
return resp, nil
}
// Validator retrieves a specific validator by address.
func (qc *queryClient) Validator(ctx context.Context, validatorAddr string) (*stakingtypes.QueryValidatorResponse, error) {
req := &stakingtypes.QueryValidatorRequest{ValidatorAddr: validatorAddr}
resp, err := qc.stakingQueryClient.Validator(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query validator %s", validatorAddr)
}
return resp, nil
}
// Delegations retrieves all delegations for a delegator.
func (qc *queryClient) Delegations(ctx context.Context, delegatorAddr string, pagination *PageRequest) (*stakingtypes.QueryDelegatorDelegationsResponse, error) {
req := &stakingtypes.QueryDelegatorDelegationsRequest{DelegatorAddr: delegatorAddr}
if pagination != nil {
req.Pagination = &query.PageRequest{
Key: pagination.Key,
Offset: pagination.Offset,
Limit: pagination.Limit,
CountTotal: pagination.CountTotal,
Reverse: pagination.Reverse,
}
}
resp, err := qc.stakingQueryClient.DelegatorDelegations(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query delegations for %s", delegatorAddr)
}
return resp, nil
}
// Delegation retrieves a specific delegation.
func (qc *queryClient) Delegation(ctx context.Context, delegatorAddr, validatorAddr string) (*stakingtypes.QueryDelegationResponse, error) {
req := &stakingtypes.QueryDelegationRequest{
DelegatorAddr: delegatorAddr,
ValidatorAddr: validatorAddr,
}
resp, err := qc.stakingQueryClient.Delegation(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrQueryFailed, "failed to query delegation from %s to %s", delegatorAddr, validatorAddr)
}
return resp, nil
}
// Tx retrieves a transaction by hash.
func (qc *queryClient) Tx(ctx context.Context, hash string) (*TxResponse, error) {
// TODO: Implement transaction query using Tendermint RPC client
// Should query /tx endpoint with transaction hash
// Parse transaction result and decode events
// Return formatted transaction response with gas usage
// Handle transaction not found errors gracefully
return nil, fmt.Errorf("transaction queries not yet implemented")
}
// TxsByEvents retrieves transactions by events.
func (qc *queryClient) TxsByEvents(ctx context.Context, events []string, pagination *PageRequest) (*TxSearchResponse, error) {
// TODO: Implement transaction search using Tendermint RPC client
// Should query /tx_search endpoint with event filters
// Support pagination with page and per_page parameters
// Parse and format transaction results with event data
// Handle complex event queries with AND/OR logic
return nil, fmt.Errorf("transaction search not yet implemented")
}
+316
View File
@@ -0,0 +1,316 @@
// Package sonr provides the main client interface for interacting with the Sonr blockchain.
package sonr
import (
"context"
"crypto/tls"
"fmt"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/keys"
"github.com/sonr-io/sonr/client/modules/did"
"github.com/sonr-io/sonr/client/modules/dwn"
"github.com/sonr-io/sonr/client/modules/svc"
"github.com/sonr-io/sonr/client/modules/ucan"
"github.com/sonr-io/sonr/client/query"
"github.com/sonr-io/sonr/client/tx"
)
// Client is the main interface for interacting with the Sonr blockchain.
// It provides access to query operations, transaction building, and module-specific functionality.
type Client interface {
// Core functionality
Query() query.QueryClient
Transaction() tx.TxBuilder
Keyring() keys.KeyringManager
// Module clients
DID() did.Client
DWN() dwn.Client
SVC() svc.Client
UCAN() ucan.Client
// Connection management
Close() error
Health(ctx context.Context) error
Config() *config.ClientConfig
}
// client implements the Client interface.
type client struct {
config *config.ClientConfig
// gRPC connections
grpcConn *grpc.ClientConn
// Module clients
queryClient query.QueryClient
txBuilder tx.TxBuilder
keyring keys.KeyringManager
didClient did.Client
dwnClient dwn.Client
svcClient svc.Client
ucanClient ucan.Client
}
// ClientOption allows customization of the client during initialization.
type ClientOption func(*clientOptions)
type clientOptions struct {
grpcDialOptions []grpc.DialOption
keyringBackend string
keyringDir string
}
// WithGRPCDialOptions allows setting custom gRPC dial options.
func WithGRPCDialOptions(opts ...grpc.DialOption) ClientOption {
return func(o *clientOptions) {
o.grpcDialOptions = append(o.grpcDialOptions, opts...)
}
}
// WithKeyringBackend sets the keyring backend (os, file, test, memory).
func WithKeyringBackend(backend string) ClientOption {
return func(o *clientOptions) {
o.keyringBackend = backend
}
}
// WithKeyringDirectory sets the directory for file-based keyring.
func WithKeyringDirectory(dir string) ClientOption {
return func(o *clientOptions) {
o.keyringDir = dir
}
}
// NewClient creates a new Sonr blockchain client with the given configuration.
func NewClient(cfg *config.ClientConfig, opts ...ClientOption) (Client, error) {
if cfg == nil {
return nil, errors.ErrMissingConfig
}
if err := cfg.Validate(); err != nil {
return nil, errors.WrapError(err, errors.ErrInvalidConfig, "client configuration validation failed")
}
// Apply options
options := &clientOptions{
keyringBackend: cfg.KeyringBackend,
keyringDir: cfg.KeyringDir,
}
for _, opt := range opts {
opt(options)
}
c := &client{
config: cfg,
}
// Initialize gRPC connection
if err := c.initGRPCConnection(options); err != nil {
return nil, errors.WrapError(err, errors.ErrConnectionFailed, "failed to initialize gRPC connection")
}
// Initialize components
if err := c.initComponents(options); err != nil {
c.Close() // Clean up on error
return nil, errors.WrapError(err, errors.ErrInvalidConfig, "failed to initialize client components")
}
return c, nil
}
// initGRPCConnection establishes the gRPC connection to the blockchain.
func (c *client) initGRPCConnection(opts *clientOptions) error {
endpoint := c.config.Network.GRPC
if endpoint == "" {
return fmt.Errorf("gRPC endpoint not configured")
}
// Build dial options
dialOpts := []grpc.DialOption{}
// Configure TLS
if c.config.Network.Insecure {
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
} else {
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
}
// Add custom dial options
dialOpts = append(dialOpts, opts.grpcDialOptions...)
// Create connection with timeout
ctx, cancel := context.WithTimeout(context.Background(), c.config.Network.RequestTimeout)
defer cancel()
conn, err := grpc.DialContext(ctx, endpoint, dialOpts...)
if err != nil {
return errors.NewConnectionError(endpoint, err)
}
c.grpcConn = conn
return nil
}
// initComponents initializes all client components.
func (c *client) initComponents(opts *clientOptions) error {
// Initialize keyring
keyringBackend := opts.keyringBackend
if keyringBackend == "" {
keyringBackend = "test" // Default for safety
}
keyringManager, err := keys.NewKeyringManager(keyringBackend, opts.keyringDir, c.config.Network.ChainID)
if err != nil {
return fmt.Errorf("failed to initialize keyring: %w", err)
}
c.keyring = keyringManager
// Initialize query client
queryClient, err := query.NewQueryClient(c.grpcConn, &c.config.Network)
if err != nil {
return fmt.Errorf("failed to initialize query client: %w", err)
}
c.queryClient = queryClient
// Initialize transaction builder
txBuilder, err := tx.NewTxBuilder(&c.config.Network, c.grpcConn)
if err != nil {
return fmt.Errorf("failed to initialize transaction builder: %w", err)
}
c.txBuilder = txBuilder
// Initialize module clients
c.didClient = did.NewClient(c.grpcConn, &c.config.Network)
c.dwnClient = dwn.NewClient(c.grpcConn, &c.config.Network)
c.svcClient = svc.NewClient(c.grpcConn, &c.config.Network)
c.ucanClient = ucan.NewClient(c.grpcConn, &c.config.Network)
return nil
}
// Query returns the query client for read operations.
func (c *client) Query() query.QueryClient {
return c.queryClient
}
// Transaction returns the transaction builder for write operations.
func (c *client) Transaction() tx.TxBuilder {
return c.txBuilder
}
// Keyring returns the keyring manager for key operations.
func (c *client) Keyring() keys.KeyringManager {
return c.keyring
}
// DID returns the DID module client.
func (c *client) DID() did.Client {
return c.didClient
}
// DWN returns the DWN module client.
func (c *client) DWN() dwn.Client {
return c.dwnClient
}
// SVC returns the SVC module client.
func (c *client) SVC() svc.Client {
return c.svcClient
}
// UCAN returns the UCAN module client.
func (c *client) UCAN() ucan.Client {
return c.ucanClient
}
// Health checks the health of the connection to the blockchain.
func (c *client) Health(ctx context.Context) error {
if c.grpcConn == nil {
return errors.ErrConnectionFailed
}
// Use a short timeout for health checks
healthCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Try to get chain info as a health check
_, err := c.queryClient.ChainInfo(healthCtx)
if err != nil {
return errors.WrapError(err, errors.ErrConnectionFailed, "health check failed")
}
return nil
}
// Config returns the client configuration.
func (c *client) Config() *config.ClientConfig {
return c.config
}
// Close closes all connections and releases resources.
func (c *client) Close() error {
var errs []error
// Close gRPC connection
if c.grpcConn != nil {
if err := c.grpcConn.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close gRPC connection: %w", err))
}
}
// Close keyring (if it supports closing)
if c.keyring != nil {
if closer, ok := c.keyring.(interface{ Close() error }); ok {
if err := closer.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close keyring: %w", err))
}
}
}
// Return combined errors if any
if len(errs) > 0 {
return fmt.Errorf("errors while closing client: %v", errs)
}
return nil
}
// NewTestClient creates a client configured for testing with sensible defaults.
func NewTestClient() (Client, error) {
cfg := config.LocalConfig()
cfg.KeyringBackend = "memory"
return NewClient(cfg)
}
// ConnectToTestnet creates a client connected to the Sonr testnet.
func ConnectToTestnet(opts ...ClientOption) (Client, error) {
cfg := config.TestnetConfig()
return NewClient(cfg, opts...)
}
// ConnectToLocal creates a client connected to a local Sonr node.
func ConnectToLocal(opts ...ClientOption) (Client, error) {
cfg := config.LocalConfig()
return NewClient(cfg, opts...)
}
// ConnectToLocalAPI creates a client connected to a local Sonr API server using localhost.
func ConnectToLocalAPI(opts ...ClientOption) (Client, error) {
cfg := config.LocalAPIConfig()
return NewClient(cfg, opts...)
}
// ConnectWithConfig creates a client with custom configuration.
func ConnectWithConfig(cfg *config.ClientConfig, opts ...ClientOption) (Client, error) {
return NewClient(cfg, opts...)
}
+357
View File
@@ -0,0 +1,357 @@
// Package tx provides transaction broadcasting utilities for the Sonr client SDK.
package tx
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
)
// BroadcastMode defines different transaction broadcasting modes.
type BroadcastMode string
const (
// BroadcastModeSync waits for the transaction to be included in a block and returns the result.
BroadcastModeSync BroadcastMode = "sync"
// BroadcastModeAsync submits the transaction and returns immediately without waiting.
BroadcastModeAsync BroadcastMode = "async"
// BroadcastModeBlock waits for the transaction to be committed and returns the full result.
BroadcastModeBlock BroadcastMode = "block"
)
// Broadcaster provides an interface for broadcasting transactions with different modes and retry logic.
type Broadcaster interface {
// Broadcasting operations
Broadcast(ctx context.Context, txBytes []byte, mode BroadcastMode) (*BroadcastResult, error)
BroadcastSync(ctx context.Context, txBytes []byte) (*BroadcastResult, error)
BroadcastAsync(ctx context.Context, txBytes []byte) (*BroadcastResult, error)
BroadcastBlock(ctx context.Context, txBytes []byte) (*BroadcastResult, error)
// Retry and monitoring
BroadcastWithRetry(ctx context.Context, txBytes []byte, mode BroadcastMode, maxRetries int) (*BroadcastResult, error)
WaitForConfirmation(ctx context.Context, txHash string, timeout time.Duration) (*TxConfirmation, error)
// Configuration
WithRetryConfig(config RetryConfig) Broadcaster
WithTimeout(timeout time.Duration) Broadcaster
}
// TxConfirmation contains information about a confirmed transaction.
type TxConfirmation struct {
TxHash string
BlockHeight int64
BlockTime time.Time
Code uint32
Log string
GasWanted int64
GasUsed int64
Events []Event
}
// RetryConfig defines retry behavior for failed broadcasts.
type RetryConfig struct {
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
BackoffFactor float64
}
// broadcaster implements Broadcaster.
type broadcaster struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
txServiceClient tx.ServiceClient
retryConfig RetryConfig
timeout time.Duration
}
// NewBroadcaster creates a new transaction broadcaster.
func NewBroadcaster(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) Broadcaster {
return &broadcaster{
grpcConn: grpcConn,
config: cfg,
txServiceClient: tx.NewServiceClient(grpcConn),
retryConfig: DefaultRetryConfig(),
timeout: 30 * time.Second,
}
}
// DefaultRetryConfig returns sensible defaults for retry configuration.
func DefaultRetryConfig() RetryConfig {
return RetryConfig{
MaxRetries: 3,
InitialDelay: 1 * time.Second,
MaxDelay: 10 * time.Second,
BackoffFactor: 2.0,
}
}
// Broadcast broadcasts a transaction with the specified mode.
func (b *broadcaster) Broadcast(ctx context.Context, txBytes []byte, mode BroadcastMode) (*BroadcastResult, error) {
// Convert our mode to SDK broadcast mode
var sdkMode tx.BroadcastMode
switch mode {
case BroadcastModeSync:
sdkMode = tx.BroadcastMode_BROADCAST_MODE_SYNC
case BroadcastModeAsync:
sdkMode = tx.BroadcastMode_BROADCAST_MODE_ASYNC
case BroadcastModeBlock:
sdkMode = tx.BroadcastMode_BROADCAST_MODE_BLOCK
default:
return nil, fmt.Errorf("invalid broadcast mode: %s", mode)
}
// Create broadcast request
req := &tx.BroadcastTxRequest{
TxBytes: txBytes,
Mode: sdkMode,
}
// Apply timeout to context
broadcastCtx, cancel := context.WithTimeout(ctx, b.timeout)
defer cancel()
// Broadcast the transaction
resp, err := b.txServiceClient.BroadcastTx(broadcastCtx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast transaction")
}
// Convert response
return convertBroadcastResponse(resp), nil
}
// BroadcastSync broadcasts a transaction synchronously.
func (b *broadcaster) BroadcastSync(ctx context.Context, txBytes []byte) (*BroadcastResult, error) {
return b.Broadcast(ctx, txBytes, BroadcastModeSync)
}
// BroadcastAsync broadcasts a transaction asynchronously.
func (b *broadcaster) BroadcastAsync(ctx context.Context, txBytes []byte) (*BroadcastResult, error) {
return b.Broadcast(ctx, txBytes, BroadcastModeAsync)
}
// BroadcastBlock broadcasts a transaction and waits for block confirmation.
func (b *broadcaster) BroadcastBlock(ctx context.Context, txBytes []byte) (*BroadcastResult, error) {
return b.Broadcast(ctx, txBytes, BroadcastModeBlock)
}
// BroadcastWithRetry broadcasts a transaction with retry logic.
func (b *broadcaster) BroadcastWithRetry(ctx context.Context, txBytes []byte, mode BroadcastMode, maxRetries int) (*BroadcastResult, error) {
var lastErr error
delay := b.retryConfig.InitialDelay
for attempt := 0; attempt <= maxRetries; attempt++ {
result, err := b.Broadcast(ctx, txBytes, mode)
if err == nil {
return result, nil
}
lastErr = err
// Don't retry on the last attempt
if attempt == maxRetries {
break
}
// Check if error is retryable
if !isRetryableError(err) {
break
}
// Wait before retrying
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
// Exponential backoff
delay = time.Duration(float64(delay) * b.retryConfig.BackoffFactor)
if delay > b.retryConfig.MaxDelay {
delay = b.retryConfig.MaxDelay
}
}
}
return nil, errors.WrapError(lastErr, errors.ErrBroadcastFailed, "failed to broadcast transaction after %d retries", maxRetries)
}
// WaitForConfirmation waits for a transaction to be confirmed on-chain.
func (b *broadcaster) WaitForConfirmation(ctx context.Context, txHash string, timeout time.Duration) (*TxConfirmation, error) {
// Create timeout context
confirmCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Poll for transaction confirmation
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-confirmCtx.Done():
return nil, errors.WrapError(confirmCtx.Err(), errors.ErrTimeout, "timeout waiting for transaction confirmation")
case <-ticker.C:
// Try to fetch transaction
req := &tx.GetTxRequest{Hash: txHash}
resp, err := b.txServiceClient.GetTx(confirmCtx, req)
if err != nil {
// Transaction not found yet, continue polling
continue
}
// Transaction found, convert to confirmation
confirmation := &TxConfirmation{
TxHash: resp.TxResponse.TxHash,
BlockHeight: resp.TxResponse.Height,
Code: resp.TxResponse.Code,
Log: resp.TxResponse.RawLog,
GasWanted: resp.TxResponse.GasWanted,
GasUsed: resp.TxResponse.GasUsed,
// BlockTime would need to be fetched from block info
}
// Convert events
for _, event := range resp.TxResponse.Events {
e := Event{
Type: event.Type,
}
for _, attr := range event.Attributes {
e.Attributes = append(e.Attributes, Attribute{
Key: attr.Key,
Value: attr.Value,
})
}
confirmation.Events = append(confirmation.Events, e)
}
return confirmation, nil
}
}
}
// WithRetryConfig sets the retry configuration.
func (b *broadcaster) WithRetryConfig(config RetryConfig) Broadcaster {
b.retryConfig = config
return b
}
// WithTimeout sets the broadcast timeout.
func (b *broadcaster) WithTimeout(timeout time.Duration) Broadcaster {
b.timeout = timeout
return b
}
// Helper functions
// convertBroadcastResponse converts SDK broadcast response to our format.
func convertBroadcastResponse(resp *tx.BroadcastTxResponse) *BroadcastResult {
result := &BroadcastResult{
TxHash: resp.TxResponse.TxHash,
Code: resp.TxResponse.Code,
Log: resp.TxResponse.RawLog,
GasWanted: resp.TxResponse.GasWanted,
GasUsed: resp.TxResponse.GasUsed,
Height: resp.TxResponse.Height,
}
// Convert events
for _, event := range resp.TxResponse.Events {
e := Event{
Type: event.Type,
}
for _, attr := range event.Attributes {
e.Attributes = append(e.Attributes, Attribute{
Key: attr.Key,
Value: attr.Value,
})
}
result.Events = append(result.Events, e)
}
return result
}
// isRetryableError determines if an error is worth retrying.
func isRetryableError(err error) bool {
// Check for specific error types that are retryable
if errors.IsConnectionError(err) {
return true
}
// Timeouts are generally retryable
if errors.GetErrorCode(err) == errors.CodeTimeout {
return true
}
// Network unreachable errors are retryable
if errors.GetErrorCode(err) == errors.CodeNetworkUnreachable {
return true
}
// Other errors like invalid transaction, insufficient funds, etc. are not retryable
return false
}
// BroadcastConfig provides configuration options for broadcasting.
type BroadcastConfig struct {
Mode BroadcastMode
Timeout time.Duration
RetryConfig RetryConfig
WaitForBlock bool
}
// DefaultBroadcastConfig returns sensible defaults for broadcasting.
func DefaultBroadcastConfig() BroadcastConfig {
return BroadcastConfig{
Mode: BroadcastModeSync,
Timeout: 30 * time.Second,
RetryConfig: DefaultRetryConfig(),
WaitForBlock: false,
}
}
// BroadcastWithConfig broadcasts a transaction using the provided configuration.
func (b *broadcaster) BroadcastWithConfig(ctx context.Context, txBytes []byte, config BroadcastConfig) (*BroadcastResult, error) {
// Set timeout
originalTimeout := b.timeout
b.timeout = config.Timeout
defer func() { b.timeout = originalTimeout }()
// Set retry config
originalRetryConfig := b.retryConfig
b.retryConfig = config.RetryConfig
defer func() { b.retryConfig = originalRetryConfig }()
// Broadcast with retry
result, err := b.BroadcastWithRetry(ctx, txBytes, config.Mode, config.RetryConfig.MaxRetries)
if err != nil {
return nil, err
}
// Wait for block confirmation if requested
if config.WaitForBlock && result.TxHash != "" {
confirmation, err := b.WaitForConfirmation(ctx, result.TxHash, config.Timeout)
if err != nil {
// Return the broadcast result even if we couldn't wait for confirmation
return result, fmt.Errorf("transaction broadcast succeeded but confirmation failed: %w", err)
}
// Update result with confirmation data
result.Height = confirmation.BlockHeight
result.Code = confirmation.Code
result.Log = confirmation.Log
result.GasWanted = confirmation.GasWanted
result.GasUsed = confirmation.GasUsed
result.Events = confirmation.Events
}
return result, nil
}
+442
View File
@@ -0,0 +1,442 @@
// Package tx provides transaction building utilities for the Sonr client SDK.
package tx
import (
"context"
"fmt"
"google.golang.org/grpc"
"cosmossdk.io/math"
sdktypes "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/keys"
)
// TxBuilder provides an interface for building and broadcasting transactions.
type TxBuilder interface {
// Transaction configuration
WithChainID(chainID string) TxBuilder
WithGasPrice(price float64, denom string) TxBuilder
WithGasLimit(limit uint64) TxBuilder
WithMemo(memo string) TxBuilder
WithTimeoutHeight(height uint64) TxBuilder
// Message operations
AddMessage(msg sdktypes.Msg) TxBuilder
AddMessages(msgs ...sdktypes.Msg) TxBuilder
ClearMessages() TxBuilder
// Fee operations
WithFee(amount sdktypes.Coins) TxBuilder
WithGasAdjustment(adjustment float64) TxBuilder
EstimateGas(ctx context.Context) (uint64, error)
// Signing and broadcasting
Sign(ctx context.Context, keyring keys.KeyringManager) (*SignedTx, error)
SignAndBroadcast(ctx context.Context, keyring keys.KeyringManager) (*BroadcastResult, error)
Broadcast(ctx context.Context, signedTx *SignedTx) (*BroadcastResult, error)
// Simulation
Simulate(ctx context.Context) (*SimulateResult, error)
// Building
Build() (*UnsignedTx, error)
BuildSigned(signature []byte, pubKey []byte) (*SignedTx, error)
// Configuration access
Config() *TxConfig
}
// TxConfig holds transaction configuration.
type TxConfig struct {
ChainID string
GasPrice float64
GasDenom string
GasLimit uint64
GasAdjustment float64
Memo string
TimeoutHeight uint64
Fee sdktypes.Coins
}
// UnsignedTx represents an unsigned transaction.
type UnsignedTx struct {
Messages []sdktypes.Msg
Config *TxConfig
SignBytes []byte
AccountNumber uint64
Sequence uint64
}
// SignedTx represents a signed transaction.
type SignedTx struct {
UnsignedTx *UnsignedTx
Signature []byte
PubKey []byte
TxBytes []byte
}
// BroadcastResult contains the result of broadcasting a transaction.
type BroadcastResult struct {
TxHash string
Code uint32
Log string
GasWanted int64
GasUsed int64
Height int64
Events []Event
}
// Event represents a transaction event.
type Event struct {
Type string
Attributes []Attribute
}
// Attribute represents an event attribute.
type Attribute struct {
Key string
Value string
}
// SimulateResult contains the result of transaction simulation.
type SimulateResult struct {
GasWanted int64
GasUsed int64
Log string
Events []Event
}
// txBuilder implements TxBuilder.
type txBuilder struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
txConfig *TxConfig
messages []sdktypes.Msg
// Cosmos SDK clients
txServiceClient tx.ServiceClient
}
// NewTxBuilder creates a new transaction builder.
func NewTxBuilder(cfg *config.NetworkConfig, grpcConn *grpc.ClientConn) (TxBuilder, error) {
if cfg == nil {
return nil, fmt.Errorf("network configuration is required")
}
if grpcConn == nil {
return nil, fmt.Errorf("gRPC connection is required")
}
txConfig := &TxConfig{
ChainID: cfg.ChainID,
GasPrice: cfg.GasPrice,
GasDenom: cfg.StakingDenom,
GasAdjustment: cfg.GasAdjustment,
GasLimit: 200000, // Default gas limit
}
return &txBuilder{
grpcConn: grpcConn,
config: cfg,
txConfig: txConfig,
messages: make([]sdktypes.Msg, 0),
txServiceClient: tx.NewServiceClient(grpcConn),
}, nil
}
// WithChainID sets the chain ID for the transaction.
func (tb *txBuilder) WithChainID(chainID string) TxBuilder {
tb.txConfig.ChainID = chainID
return tb
}
// WithGasPrice sets the gas price and denomination.
func (tb *txBuilder) WithGasPrice(price float64, denom string) TxBuilder {
tb.txConfig.GasPrice = price
tb.txConfig.GasDenom = denom
return tb
}
// WithGasLimit sets the gas limit for the transaction.
func (tb *txBuilder) WithGasLimit(limit uint64) TxBuilder {
tb.txConfig.GasLimit = limit
return tb
}
// WithMemo sets the memo for the transaction.
func (tb *txBuilder) WithMemo(memo string) TxBuilder {
tb.txConfig.Memo = memo
return tb
}
// WithTimeoutHeight sets the timeout height for the transaction.
func (tb *txBuilder) WithTimeoutHeight(height uint64) TxBuilder {
tb.txConfig.TimeoutHeight = height
return tb
}
// AddMessage adds a single message to the transaction.
func (tb *txBuilder) AddMessage(msg sdktypes.Msg) TxBuilder {
tb.messages = append(tb.messages, msg)
return tb
}
// AddMessages adds multiple messages to the transaction.
func (tb *txBuilder) AddMessages(msgs ...sdktypes.Msg) TxBuilder {
tb.messages = append(tb.messages, msgs...)
return tb
}
// ClearMessages removes all messages from the transaction.
func (tb *txBuilder) ClearMessages() TxBuilder {
tb.messages = make([]sdktypes.Msg, 0)
return tb
}
// WithFee sets the transaction fee directly.
func (tb *txBuilder) WithFee(amount sdktypes.Coins) TxBuilder {
tb.txConfig.Fee = amount
return tb
}
// WithGasAdjustment sets the gas adjustment factor.
func (tb *txBuilder) WithGasAdjustment(adjustment float64) TxBuilder {
tb.txConfig.GasAdjustment = adjustment
return tb
}
// EstimateGas estimates the gas required for the transaction.
func (tb *txBuilder) EstimateGas(ctx context.Context) (uint64, error) {
// Build unsigned transaction for simulation
_, err := tb.Build()
if err != nil {
return 0, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to build transaction for gas estimation")
}
// Simulate the transaction
simulateResult, err := tb.Simulate(ctx)
if err != nil {
return 0, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to simulate transaction")
}
// Apply gas adjustment
estimatedGas := float64(simulateResult.GasUsed) * tb.txConfig.GasAdjustment
return uint64(estimatedGas), nil
}
// Sign signs the transaction using the provided keyring.
func (tb *txBuilder) Sign(ctx context.Context, keyring keys.KeyringManager) (*SignedTx, error) {
// Build unsigned transaction
unsignedTx, err := tb.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to build unsigned transaction")
}
// Sign the transaction bytes using the DWN plugin
signature, err := keyring.SignTransaction(ctx, unsignedTx.SignBytes)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to sign transaction")
}
// Get wallet identity for public key
identity, err := keyring.GetIssuerDID(ctx)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to get wallet identity")
}
// For now, use a placeholder for public key - this should be derived from the DID
// TODO: Extract public key from DID or add GetPubKey method to KeyringManager
pubKey := []byte(identity.DID) // Placeholder
// Build signed transaction
signedTx, err := tb.BuildSigned(signature.Signature, pubKey)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to build signed transaction")
}
return signedTx, nil
}
// SignAndBroadcast signs and broadcasts the transaction in one operation.
func (tb *txBuilder) SignAndBroadcast(ctx context.Context, keyring keys.KeyringManager) (*BroadcastResult, error) {
// Sign the transaction
signedTx, err := tb.Sign(ctx, keyring)
if err != nil {
return nil, err
}
// Broadcast the signed transaction
return tb.Broadcast(ctx, signedTx)
}
// Broadcast broadcasts a signed transaction to the network.
func (tb *txBuilder) Broadcast(ctx context.Context, signedTx *SignedTx) (*BroadcastResult, error) {
// Create broadcast request
req := &tx.BroadcastTxRequest{
TxBytes: signedTx.TxBytes,
Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC, // Default to sync mode
}
// Broadcast the transaction
resp, err := tb.txServiceClient.BroadcastTx(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast transaction")
}
// Convert response to our format
result := &BroadcastResult{
TxHash: resp.TxResponse.TxHash,
Code: resp.TxResponse.Code,
Log: resp.TxResponse.RawLog,
GasWanted: resp.TxResponse.GasWanted,
GasUsed: resp.TxResponse.GasUsed,
Height: resp.TxResponse.Height,
}
// Convert events
for _, event := range resp.TxResponse.Events {
e := Event{
Type: event.Type,
}
for _, attr := range event.Attributes {
e.Attributes = append(e.Attributes, Attribute{
Key: attr.Key,
Value: attr.Value,
})
}
result.Events = append(result.Events, e)
}
return result, nil
}
// Simulate simulates the transaction to estimate gas and check for errors.
func (tb *txBuilder) Simulate(ctx context.Context) (*SimulateResult, error) {
// Build unsigned transaction for simulation
unsignedTx, err := tb.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to build transaction for simulation")
}
// Create simulate request
req := &tx.SimulateRequest{
TxBytes: unsignedTx.SignBytes, // Use sign bytes for simulation
}
// Simulate the transaction
resp, err := tb.txServiceClient.Simulate(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to simulate transaction")
}
// Convert response to our format
result := &SimulateResult{
GasWanted: int64(resp.GasInfo.GasWanted),
GasUsed: int64(resp.GasInfo.GasUsed),
Log: resp.Result.Log,
}
// Convert events
for _, event := range resp.Result.Events {
e := Event{
Type: event.Type,
}
for _, attr := range event.Attributes {
e.Attributes = append(e.Attributes, Attribute{
Key: attr.Key,
Value: attr.Value,
})
}
result.Events = append(result.Events, e)
}
return result, nil
}
// Build creates an unsigned transaction.
func (tb *txBuilder) Build() (*UnsignedTx, error) {
// Allow building without messages for testing/simulation purposes
// Real transactions will still require messages when broadcasting
// Calculate fee if not set
fee := tb.txConfig.Fee
if fee.IsZero() {
// Calculate fee based on gas price and limit
gasAmount := math.NewIntFromUint64(uint64(float64(tb.txConfig.GasLimit) * tb.txConfig.GasPrice))
fee = sdktypes.NewCoins(sdktypes.NewCoin(tb.txConfig.GasDenom, gasAmount))
}
// Create sign bytes (simplified - in a real implementation this would use proper transaction encoding)
signBytes := []byte(fmt.Sprintf("chain_id:%s,messages:%d,fee:%s,memo:%s",
tb.txConfig.ChainID,
len(tb.messages),
fee.String(),
tb.txConfig.Memo))
return &UnsignedTx{
Messages: tb.messages,
Config: tb.txConfig,
SignBytes: signBytes,
// TODO: Fetch account number and sequence from chain
AccountNumber: 0,
Sequence: 0,
}, nil
}
// BuildSigned creates a signed transaction from signature and public key.
func (tb *txBuilder) BuildSigned(signature []byte, pubKey []byte) (*SignedTx, error) {
unsignedTx, err := tb.Build()
if err != nil {
return nil, err
}
// Create transaction bytes (simplified - in a real implementation this would use proper transaction encoding)
txBytes := append(unsignedTx.SignBytes, signature...)
txBytes = append(txBytes, pubKey...)
return &SignedTx{
UnsignedTx: unsignedTx,
Signature: signature,
PubKey: pubKey,
TxBytes: txBytes,
}, nil
}
// Config returns the current transaction configuration.
func (tb *txBuilder) Config() *TxConfig {
return tb.txConfig
}
// Utility functions
// NewTxConfig creates a new transaction configuration with defaults.
func NewTxConfig(chainID string) *TxConfig {
return &TxConfig{
ChainID: chainID,
GasPrice: 0.001,
GasDenom: "usnr",
GasAdjustment: 1.5,
GasLimit: 200000,
}
}
// DefaultGasLimit returns the default gas limit for transactions.
func DefaultGasLimit() uint64 {
return 200000
}
// DefaultGasPrice returns the default gas price for the Sonr network.
func DefaultGasPrice() float64 {
return 0.001
}
// CalculateFee calculates the transaction fee based on gas price and limit.
func CalculateFee(gasPrice float64, gasLimit uint64, denom string) sdktypes.Coins {
gasAmount := math.NewIntFromUint64(uint64(float64(gasLimit) * gasPrice))
return sdktypes.NewCoins(sdktypes.NewCoin(denom, gasAmount))
}
+191
View File
@@ -0,0 +1,191 @@
package tx
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/sonr-io/sonr/client/config"
)
// TxBuilderTestSuite tests the transaction builder.
type TxBuilderTestSuite struct {
suite.Suite
builder TxBuilder
config *config.NetworkConfig
}
func (suite *TxBuilderTestSuite) SetupTest() {
cfg := config.LocalNetwork()
suite.config = &cfg
// Create mock gRPC connection for testing
conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
suite.Require().NoError(err)
builder, err := NewTxBuilder(suite.config, conn)
suite.Require().NoError(err)
suite.builder = builder
}
func (suite *TxBuilderTestSuite) TestAddMessage() {
// Create a test message
msg := &banktypes.MsgSend{
FromAddress: "sonr1xyz...",
ToAddress: "sonr1abc...",
Amount: sdk.NewCoins(sdk.NewInt64Coin("usnr", 1000)),
}
// Add message
suite.builder.AddMessage(msg)
// Verify message was added
unsignedTx, err := suite.builder.Build()
suite.Require().NoError(err)
suite.Require().NotNil(unsignedTx)
suite.Require().Len(unsignedTx.Messages, 1)
}
func (suite *TxBuilderTestSuite) TestWithMemo() {
memo := "test transaction"
suite.builder = suite.builder.WithMemo(memo)
unsignedTx, err := suite.builder.Build()
suite.Require().NoError(err)
suite.Require().NotNil(unsignedTx)
// Memo is set internally in the transaction
}
func (suite *TxBuilderTestSuite) TestWithGasLimit() {
gasLimit := uint64(200000)
suite.builder = suite.builder.WithGasLimit(gasLimit)
unsignedTx, err := suite.builder.Build()
suite.Require().NoError(err)
suite.Require().NotNil(unsignedTx)
// Gas limit is set internally
}
func (suite *TxBuilderTestSuite) TestWithFee() {
fee := sdk.NewCoins(sdk.NewInt64Coin("usnr", 5000))
suite.builder = suite.builder.WithFee(fee)
unsignedTx, err := suite.builder.Build()
suite.Require().NoError(err)
suite.Require().NotNil(unsignedTx)
// Fee is set internally
}
func (suite *TxBuilderTestSuite) TestClearMessages() {
// Add some data
msg := &banktypes.MsgSend{
FromAddress: "sonr1xyz...",
ToAddress: "sonr1abc...",
Amount: sdk.NewCoins(sdk.NewInt64Coin("usnr", 1000)),
}
suite.builder = suite.builder.AddMessage(msg)
suite.builder = suite.builder.WithMemo("test")
// Clear messages
suite.builder = suite.builder.ClearMessages()
// Build should create transaction with no messages
unsignedTx, err := suite.builder.Build()
suite.Require().NoError(err)
suite.Require().Len(unsignedTx.Messages, 0)
}
func (suite *TxBuilderTestSuite) TestMultipleMessages() {
// Add multiple messages
msg1 := &banktypes.MsgSend{
FromAddress: "sonr1xyz...",
ToAddress: "sonr1abc...",
Amount: sdk.NewCoins(sdk.NewInt64Coin("usnr", 1000)),
}
msg2 := &banktypes.MsgSend{
FromAddress: "sonr1abc...",
ToAddress: "sonr1def...",
Amount: sdk.NewCoins(sdk.NewInt64Coin("usnr", 2000)),
}
suite.builder.AddMessage(msg1)
suite.builder.AddMessage(msg2)
unsignedTx, err := suite.builder.Build()
suite.Require().NoError(err)
suite.Require().Len(unsignedTx.Messages, 2)
}
func TestTxBuilderTestSuite(t *testing.T) {
suite.Run(t, new(TxBuilderTestSuite))
}
// TestTxBuilderValidation tests transaction builder validation.
func TestTxBuilderValidation(t *testing.T) {
tests := []struct {
name string
setup func(TxBuilder) TxBuilder
wantError bool
errorMsg string
}{
{
name: "valid transaction",
setup: func(b TxBuilder) TxBuilder {
msg := &banktypes.MsgSend{
FromAddress: "sonr1xyz...",
ToAddress: "sonr1abc...",
Amount: sdk.NewCoins(sdk.NewInt64Coin("usnr", 1000)),
}
return b.AddMessage(msg).WithGasLimit(100000)
},
wantError: false,
},
{
name: "no messages",
setup: func(b TxBuilder) TxBuilder {
return b.WithGasLimit(100000)
},
wantError: false, // Empty transactions are technically valid
},
{
name: "zero gas limit",
setup: func(b TxBuilder) TxBuilder {
msg := &banktypes.MsgSend{
FromAddress: "sonr1xyz...",
ToAddress: "sonr1abc...",
Amount: sdk.NewCoins(sdk.NewInt64Coin("usnr", 1000)),
}
return b.AddMessage(msg).WithGasLimit(0)
},
wantError: false, // Zero gas is allowed for simulation
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := config.LocalNetwork()
conn, _ := grpc.Dial("localhost:9090", grpc.WithInsecure())
builder, err := NewTxBuilder(&cfg, conn)
require.NoError(t, err)
builder = tt.setup(builder)
_, err = builder.Build()
if tt.wantError {
require.Error(t, err)
if tt.errorMsg != "" {
require.Contains(t, err.Error(), tt.errorMsg)
}
} else {
require.NoError(t, err)
}
})
}
}
+316
View File
@@ -0,0 +1,316 @@
// Package tx provides gas estimation and fee calculation utilities for the Sonr client SDK.
package tx
import (
"context"
"fmt"
"math"
"google.golang.org/grpc"
sdktypes "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
)
// GasEstimator provides an interface for estimating gas costs and calculating fees.
type GasEstimator interface {
// Gas estimation
EstimateGas(ctx context.Context, msgs []sdktypes.Msg) (*GasEstimate, error)
EstimateGasForTx(ctx context.Context, unsignedTx *UnsignedTx) (*GasEstimate, error)
// Fee calculation
CalculateFee(gasUsed uint64, gasPrice float64, denom string) sdktypes.Coins
CalculateFeeWithAdjustment(gasUsed uint64, gasPrice float64, adjustment float64, denom string) sdktypes.Coins
// Gas configuration
WithGasAdjustment(adjustment float64) GasEstimator
WithMinGasPrice(price float64) GasEstimator
WithMaxGasLimit(limit uint64) GasEstimator
// Utility methods
GetRecommendedGasPrice(ctx context.Context) (float64, error)
GetNetworkGasInfo(ctx context.Context) (*NetworkGasInfo, error)
}
// GasEstimate contains the result of gas estimation.
type GasEstimate struct {
GasWanted uint64 // Estimated gas needed
GasUsed uint64 // Gas used in simulation
GasLimit uint64 // Recommended gas limit (with adjustment)
Fee sdktypes.Coins // Calculated fee
GasPrice float64 // Gas price used
GasAdjustment float64 // Adjustment factor applied
}
// NetworkGasInfo contains network-wide gas information.
type NetworkGasInfo struct {
MinGasPrice float64 // Minimum gas price accepted by validators
MedianGasPrice float64 // Median gas price from recent transactions
RecommendedGasPrice float64 // Recommended gas price for fast inclusion
MaxGasLimit uint64 // Maximum gas limit per transaction
}
// GasConfig holds gas estimation configuration.
type GasConfig struct {
Adjustment float64 // Gas adjustment factor (default: 1.5)
MinGasPrice float64 // Minimum gas price
MaxGasLimit uint64 // Maximum gas limit
Denom string // Gas fee denomination
}
// gasEstimator implements GasEstimator.
type gasEstimator struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
txServiceClient tx.ServiceClient
gasConfig GasConfig
}
// NewGasEstimator creates a new gas estimator.
func NewGasEstimator(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) GasEstimator {
gasConfig := GasConfig{
Adjustment: cfg.GasAdjustment,
MinGasPrice: cfg.GasPrice,
MaxGasLimit: 10000000, // 10M gas limit
Denom: cfg.StakingDenom,
}
return &gasEstimator{
grpcConn: grpcConn,
config: cfg,
txServiceClient: tx.NewServiceClient(grpcConn),
gasConfig: gasConfig,
}
}
// EstimateGas estimates gas for a list of messages.
func (ge *gasEstimator) EstimateGas(ctx context.Context, msgs []sdktypes.Msg) (*GasEstimate, error) {
if len(msgs) == 0 {
return nil, fmt.Errorf("no messages provided for gas estimation")
}
// Create a temporary transaction builder to build the transaction for simulation
builder, err := NewTxBuilder(ge.config, ge.grpcConn)
if err != nil {
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to create transaction builder")
}
// Add messages and build unsigned transaction
for _, msg := range msgs {
builder.AddMessage(msg)
}
unsignedTx, err := builder.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to build transaction for estimation")
}
return ge.EstimateGasForTx(ctx, unsignedTx)
}
// EstimateGasForTx estimates gas for an unsigned transaction.
func (ge *gasEstimator) EstimateGasForTx(ctx context.Context, unsignedTx *UnsignedTx) (*GasEstimate, error) {
// Create simulate request
req := &tx.SimulateRequest{
TxBytes: unsignedTx.SignBytes, // Use sign bytes for simulation
}
// Simulate the transaction
resp, err := ge.txServiceClient.Simulate(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to simulate transaction")
}
gasUsed := resp.GasInfo.GasUsed
gasWanted := resp.GasInfo.GasWanted
// Apply gas adjustment
gasLimit := uint64(float64(gasUsed) * ge.gasConfig.Adjustment)
// Ensure gas limit doesn't exceed maximum
if gasLimit > ge.gasConfig.MaxGasLimit {
gasLimit = ge.gasConfig.MaxGasLimit
}
// Calculate fee
fee := ge.CalculateFee(gasLimit, ge.gasConfig.MinGasPrice, ge.gasConfig.Denom)
return &GasEstimate{
GasWanted: gasWanted,
GasUsed: gasUsed,
GasLimit: gasLimit,
Fee: fee,
GasPrice: ge.gasConfig.MinGasPrice,
GasAdjustment: ge.gasConfig.Adjustment,
}, nil
}
// CalculateFee calculates the transaction fee based on gas usage and price.
func (ge *gasEstimator) CalculateFee(gasUsed uint64, gasPrice float64, denom string) sdktypes.Coins {
// Calculate fee amount
feeAmount := math.Ceil(float64(gasUsed) * gasPrice)
// Create coin
feeCoin := sdktypes.NewInt64Coin(denom, int64(feeAmount))
return sdktypes.NewCoins(feeCoin)
}
// CalculateFeeWithAdjustment calculates fee with a custom gas adjustment.
func (ge *gasEstimator) CalculateFeeWithAdjustment(gasUsed uint64, gasPrice float64, adjustment float64, denom string) sdktypes.Coins {
adjustedGas := uint64(float64(gasUsed) * adjustment)
return ge.CalculateFee(adjustedGas, gasPrice, denom)
}
// WithGasAdjustment sets the gas adjustment factor.
func (ge *gasEstimator) WithGasAdjustment(adjustment float64) GasEstimator {
ge.gasConfig.Adjustment = adjustment
return ge
}
// WithMinGasPrice sets the minimum gas price.
func (ge *gasEstimator) WithMinGasPrice(price float64) GasEstimator {
ge.gasConfig.MinGasPrice = price
return ge
}
// WithMaxGasLimit sets the maximum gas limit.
func (ge *gasEstimator) WithMaxGasLimit(limit uint64) GasEstimator {
ge.gasConfig.MaxGasLimit = limit
return ge
}
// GetRecommendedGasPrice returns the recommended gas price for the network.
func (ge *gasEstimator) GetRecommendedGasPrice(ctx context.Context) (float64, error) {
// TODO: Implement dynamic gas price discovery based on network conditions
// Should query recent transactions to analyze gas price trends
// Calculate percentile-based recommendations (e.g., 25th, 50th, 75th)
// Consider network congestion and validator preferences
// Return optimal gas price for desired transaction inclusion speed
return ge.gasConfig.MinGasPrice, nil
}
// GetNetworkGasInfo retrieves network-wide gas information.
func (ge *gasEstimator) GetNetworkGasInfo(ctx context.Context) (*NetworkGasInfo, error) {
// TODO: Implement dynamic network gas info retrieval
// Should query validator minimum gas prices via gRPC
// Analyze recent block gas usage patterns and limits
// Calculate median and recommended gas prices from mempool
// Monitor network congestion metrics for pricing recommendations
// Query chain parameters for maximum gas limits and constraints
return &NetworkGasInfo{
MinGasPrice: ge.gasConfig.MinGasPrice,
MedianGasPrice: ge.gasConfig.MinGasPrice,
RecommendedGasPrice: ge.gasConfig.MinGasPrice,
MaxGasLimit: ge.gasConfig.MaxGasLimit,
}, nil
}
// Utility functions and constants
// Default gas values for different transaction types
const (
// DefaultGasLimitValue is the default gas limit for transactions
DefaultGasLimitValue = 200000
// SendGasLimit is the typical gas limit for send transactions
SendGasLimit = 100000
// DelegateGasLimit is the typical gas limit for delegation transactions
DelegateGasLimit = 150000
// ContractCallGasLimit is the typical gas limit for smart contract calls
ContractCallGasLimit = 500000
// MinGasAdjustment is the minimum recommended gas adjustment
MinGasAdjustment = 1.1
// MaxGasAdjustment is the maximum reasonable gas adjustment
MaxGasAdjustment = 3.0
)
// GasLimitForMessageType returns a recommended gas limit for different message types.
func GasLimitForMessageType(msgType string) uint64 {
switch msgType {
case "/cosmos.bank.v1beta1.MsgSend":
return SendGasLimit
case "/cosmos.staking.v1beta1.MsgDelegate":
return DelegateGasLimit
case "/cosmos.staking.v1beta1.MsgUndelegate":
return DelegateGasLimit
case "/cosmos.staking.v1beta1.MsgRedelegate":
return DelegateGasLimit * 2
default:
return DefaultGasLimitValue
}
}
// EstimateGasForMessages provides a quick gas estimate based on message types.
func EstimateGasForMessages(msgs []sdktypes.Msg) uint64 {
var totalGas uint64
for _, msg := range msgs {
msgType := sdktypes.MsgTypeURL(msg)
gas := GasLimitForMessageType(msgType)
totalGas += gas
}
// Add base transaction overhead
totalGas += 50000
return totalGas
}
// ValidateGasPrice checks if a gas price is reasonable.
func ValidateGasPrice(gasPrice float64) error {
if gasPrice <= 0 {
return fmt.Errorf("gas price must be positive")
}
if gasPrice > 1.0 { // 1 SNR per gas unit seems excessive
return fmt.Errorf("gas price %f seems too high", gasPrice)
}
return nil
}
// ValidateGasLimit checks if a gas limit is reasonable.
func ValidateGasLimit(gasLimit uint64) error {
if gasLimit == 0 {
return fmt.Errorf("gas limit must be positive")
}
if gasLimit > 50000000 { // 50M gas limit seems excessive
return fmt.Errorf("gas limit %d seems too high", gasLimit)
}
return nil
}
// OptimizeGasConfig optimizes gas configuration based on network conditions.
func OptimizeGasConfig(config *GasConfig, networkInfo *NetworkGasInfo) *GasConfig {
optimized := *config
// Use recommended gas price if it's higher than our minimum
if networkInfo.RecommendedGasPrice > config.MinGasPrice {
optimized.MinGasPrice = networkInfo.RecommendedGasPrice
}
// Ensure gas adjustment is within reasonable bounds
if optimized.Adjustment < MinGasAdjustment {
optimized.Adjustment = MinGasAdjustment
}
if optimized.Adjustment > MaxGasAdjustment {
optimized.Adjustment = MaxGasAdjustment
}
// Use network max gas limit if it's lower than our configured max
if networkInfo.MaxGasLimit > 0 && networkInfo.MaxGasLimit < config.MaxGasLimit {
optimized.MaxGasLimit = networkInfo.MaxGasLimit
}
return &optimized
}
+268
View File
@@ -0,0 +1,268 @@
package tx
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/sonr-io/sonr/client/config"
)
// GasEstimatorTestSuite tests the gas estimator.
type GasEstimatorTestSuite struct {
suite.Suite
estimator GasEstimator
config *config.NetworkConfig
}
func (suite *GasEstimatorTestSuite) SetupTest() {
cfg := config.LocalNetwork()
suite.config = &cfg
// Create mock gRPC connection for testing
conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
suite.Require().NoError(err)
suite.estimator = NewGasEstimator(conn, suite.config)
}
func (suite *GasEstimatorTestSuite) TestCalculateFee() {
gasUsed := uint64(100000)
gasPrice := 0.025
denom := "usnr"
fee := suite.estimator.CalculateFee(gasUsed, gasPrice, denom)
suite.Require().NotNil(fee)
suite.Require().Len(fee, 1)
suite.Require().Equal(denom, fee[0].Denom)
suite.Require().Equal(int64(2500), fee[0].Amount.Int64())
}
func (suite *GasEstimatorTestSuite) TestCalculateFeeWithAdjustment() {
gasUsed := uint64(100000)
gasPrice := 0.025
adjustment := 1.5
denom := "usnr"
fee := suite.estimator.CalculateFeeWithAdjustment(gasUsed, gasPrice, adjustment, denom)
suite.Require().NotNil(fee)
suite.Require().Len(fee, 1)
suite.Require().Equal(denom, fee[0].Denom)
suite.Require().Equal(int64(3750), fee[0].Amount.Int64())
}
func (suite *GasEstimatorTestSuite) TestWithGasAdjustment() {
adjustment := 2.0
updated := suite.estimator.WithGasAdjustment(adjustment)
suite.Require().NotNil(updated)
// Verify adjustment was applied
ge := updated.(*gasEstimator)
suite.Require().Equal(adjustment, ge.gasConfig.Adjustment)
}
func (suite *GasEstimatorTestSuite) TestWithMinGasPrice() {
price := 0.05
updated := suite.estimator.WithMinGasPrice(price)
suite.Require().NotNil(updated)
// Verify price was applied
ge := updated.(*gasEstimator)
suite.Require().Equal(price, ge.gasConfig.MinGasPrice)
}
func (suite *GasEstimatorTestSuite) TestWithMaxGasLimit() {
limit := uint64(5000000)
updated := suite.estimator.WithMaxGasLimit(limit)
suite.Require().NotNil(updated)
// Verify limit was applied
ge := updated.(*gasEstimator)
suite.Require().Equal(limit, ge.gasConfig.MaxGasLimit)
}
func (suite *GasEstimatorTestSuite) TestGetRecommendedGasPrice() {
price, err := suite.estimator.GetRecommendedGasPrice(context.Background())
suite.Require().NoError(err)
suite.Require().Greater(price, 0.0)
}
func (suite *GasEstimatorTestSuite) TestGetNetworkGasInfo() {
info, err := suite.estimator.GetNetworkGasInfo(context.Background())
suite.Require().NoError(err)
suite.Require().NotNil(info)
suite.Require().Greater(info.MinGasPrice, 0.0)
suite.Require().Greater(info.MaxGasLimit, uint64(0))
}
func TestGasEstimatorTestSuite(t *testing.T) {
suite.Run(t, new(GasEstimatorTestSuite))
}
// TestGasLimitForMessageType tests gas limit recommendations.
func TestGasLimitForMessageType(t *testing.T) {
tests := []struct {
msgType string
expectedGas uint64
}{
{
msgType: "/cosmos.bank.v1beta1.MsgSend",
expectedGas: SendGasLimit,
},
{
msgType: "/cosmos.staking.v1beta1.MsgDelegate",
expectedGas: DelegateGasLimit,
},
{
msgType: "/cosmos.staking.v1beta1.MsgUndelegate",
expectedGas: DelegateGasLimit,
},
{
msgType: "/cosmos.staking.v1beta1.MsgRedelegate",
expectedGas: DelegateGasLimit * 2,
},
{
msgType: "/unknown.message.type",
expectedGas: DefaultGasLimitValue,
},
}
for _, tt := range tests {
t.Run(tt.msgType, func(t *testing.T) {
gas := GasLimitForMessageType(tt.msgType)
require.Equal(t, tt.expectedGas, gas)
})
}
}
// TestEstimateGasForMessages tests quick gas estimation.
func TestEstimateGasForMessages(t *testing.T) {
msgs := []sdk.Msg{
&banktypes.MsgSend{
FromAddress: "sonr1xyz...",
ToAddress: "sonr1abc...",
Amount: sdk.NewCoins(sdk.NewInt64Coin("usnr", 1000)),
},
}
gas := EstimateGasForMessages(msgs)
// Should be SendGasLimit + base overhead
expected := uint64(SendGasLimit + 50000)
require.Equal(t, expected, gas)
}
// TestValidateGasPrice tests gas price validation.
func TestValidateGasPrice(t *testing.T) {
tests := []struct {
name string
gasPrice float64
wantError bool
}{
{
name: "valid gas price",
gasPrice: 0.025,
wantError: false,
},
{
name: "zero gas price",
gasPrice: 0,
wantError: true,
},
{
name: "negative gas price",
gasPrice: -0.1,
wantError: true,
},
{
name: "excessive gas price",
gasPrice: 2.0,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateGasPrice(tt.gasPrice)
if tt.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// TestValidateGasLimit tests gas limit validation.
func TestValidateGasLimit(t *testing.T) {
tests := []struct {
name string
gasLimit uint64
wantError bool
}{
{
name: "valid gas limit",
gasLimit: 200000,
wantError: false,
},
{
name: "zero gas limit",
gasLimit: 0,
wantError: true,
},
{
name: "excessive gas limit",
gasLimit: 100000000,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateGasLimit(tt.gasLimit)
if tt.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// TestOptimizeGasConfig tests gas configuration optimization.
func TestOptimizeGasConfig(t *testing.T) {
config := &GasConfig{
Adjustment: 0.5, // Too low
MinGasPrice: 0.01,
MaxGasLimit: 10000000,
Denom: "usnr",
}
networkInfo := &NetworkGasInfo{
MinGasPrice: 0.025,
MedianGasPrice: 0.03,
RecommendedGasPrice: 0.035,
MaxGasLimit: 5000000,
}
optimized := OptimizeGasConfig(config, networkInfo)
// Should use recommended gas price
require.Equal(t, networkInfo.RecommendedGasPrice, optimized.MinGasPrice)
// Should adjust to minimum adjustment
require.Equal(t, MinGasAdjustment, optimized.Adjustment)
// Should use network max gas limit
require.Equal(t, networkInfo.MaxGasLimit, optimized.MaxGasLimit)
}