Feat/add networks (#1303)

* No commit suggestions generated

* No commit suggestions generated

* No commit suggestions generated
This commit is contained in:
Prad Nukala
2025-10-12 11:23:20 -04:00
committed by GitHub
parent 40eadc995e
commit ebfa2b062a
97 changed files with 3979 additions and 16119 deletions
-93
View File
@@ -1,93 +0,0 @@
# Caddy reverse proxy configuration for Sonr services
{
# Global options
admin off
auto_https off
# Enable debug mode for development
debug
}
# Health check endpoint
:80/health {
respond "OK" 200
}
# REST API proxy (Cosmos SDK)
:80/api/* {
reverse_proxy snrd:1317
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
}
# gRPC-Web proxy
:80/grpc/* {
reverse_proxy h2c://snrd:9090
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, x-grpc-web, x-user-agent"
}
# JSON-RPC proxy (EVM)
:80/rpc {
reverse_proxy snrd:8545
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type"
}
# WebSocket JSON-RPC proxy (EVM)
:80/ws {
reverse_proxy snrd:8546
header Access-Control-Allow-Origin *
}
# Highway service proxy
:80/highway/* {
reverse_proxy highway:8090
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
}
# Production configuration with TLS (when CADDY_DOMAIN is set)
{$CADDY_DOMAIN:localhost} {
# TLS configuration for production
@production {
not host localhost
}
tls @production {
# Automatic HTTPS with Let's Encrypt
}
# Same proxy rules as above
handle /health {
respond "OK" 200
}
handle /api/* {
reverse_proxy snrd:1317
}
handle /grpc/* {
reverse_proxy h2c://snrd:9090
}
handle /rpc {
reverse_proxy snrd:8545
}
handle /ws {
reverse_proxy snrd:8546
}
handle /highway/* {
reverse_proxy highway:8090
}
# Default handler
handle {
respond "Sonr Network Gateway" 200
}
}
+11 -7
View File
@@ -1,5 +1,5 @@
# Use build argument for base image to allow using pre-cached base
ARG BASE_IMAGE=golang:1.24.7-alpine3.22
ARG BASE_IMAGE=golang:1.25-alpine3.22
FROM ${BASE_IMAGE} AS builder
SHELL ["/bin/sh", "-ecuxo", "pipefail"]
@@ -26,7 +26,7 @@ RUN git config --global --add safe.directory /code
RUN --mount=type=cache,target=/tmp/wasmvm \
set -eux; \
export ARCH=$(uname -m); \
WASM_VERSION=$(go list -m all | grep github.com/CosmWasm/wasmvm | head -1 || echo ""); \
WASM_VERSION=$(GOTOOLCHAIN=auto go list -m all | grep github.com/CosmWasm/wasmvm | head -1 || echo ""); \
if [ ! -z "${WASM_VERSION}" ]; then \
WASMVM_REPO=$(echo $WASM_VERSION | awk '{print $1}'); \
WASMVM_VERS=$(echo $WASM_VERSION | awk '{print $2}'); \
@@ -40,7 +40,7 @@ RUN --mount=type=cache,target=/tmp/wasmvm \
# Download Go modules
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
GOTOOLCHAIN=auto go mod download
# Build binary with optimizations
RUN --mount=type=cache,target=/go/pkg/mod \
@@ -50,7 +50,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
COMMIT=$(git log -1 --format='%H' 2>/dev/null || echo "unknown"); \
LEDGER_ENABLED=false BUILD_TAGS=muslc LINK_STATICALLY=true \
CGO_ENABLED=1 GOOS=linux \
go build \
GOTOOLCHAIN=auto go build \
-mod=readonly \
-tags "netgo,ledger,muslc" \
-ldflags "-X github.com/cosmos/cosmos-sdk/version.Name=sonr \
@@ -91,7 +91,7 @@ RUN git config --global --add safe.directory /code
# Download Go modules
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
GOTOOLCHAIN=auto go mod download
# Build Highway binary with optimizations
RUN --mount=type=cache,target=/go/pkg/mod \
@@ -156,8 +156,12 @@ LABEL org.opencontainers.image.source="https://github.com/sonr-io/sonr"
# Copy binary from build stage
COPY --from=builder /code/build/snrd /usr/bin
COPY --from=builder /lib/libwasmvm_muslc.a /lib/libwasmvm_muslc.a
COPY --from=builder /code/scripts/test_node.sh /usr/bin/testnet
RUN chmod +x /usr/bin/testnet
# Copy runtime scripts and make them executable
COPY --from=builder /code/scripts/test_node.sh /usr/bin/devnet
COPY --from=builder /code/scripts/testnet-setup.sh /usr/bin/testnet-setup.sh
COPY --from=builder /code/scripts/lib/ /usr/local/lib/sonr-scripts/
RUN chmod +x /usr/bin/devnet /usr/bin/testnet-setup.sh /usr/local/lib/sonr-scripts/*.sh
# Set up dependencies
ENV PACKAGES="curl make bash jq sed"
-847
View File
@@ -1,847 +0,0 @@
# Sonr Cryptography Library Migration Context
> **Repository Migration**: `sonr-io/sonr/crypto/` → `sonr-io/crypto`
> **Package Name**: `github.com/sonr-io/crypto`
> **Current Version**: `v1.0.1`
## Overview
The Sonr Cryptography Library is a comprehensive collection of cryptographic primitives designed for secure decentralized applications. It provides enterprise-grade implementations of elliptic curve cryptography, multi-party computation, threshold cryptography, zero-knowledge proofs, and advanced signature schemes.
**Key Features:**
- **Multi-Party Computation (MPC)**: Secure distributed key generation and signing
- **Threshold Cryptography**: TECDSA and TED25519 with FROST protocol
- **Advanced Signatures**: BLS aggregation, BBS+ selective disclosure, Schnorr variants
- **Secret Sharing**: Shamir, Feldman VSS, Pedersen VSS implementations
- **Zero-Knowledge Proofs**: Bulletproofs for range proofs
- **Multiple Elliptic Curves**: Ed25519, Secp256k1, P-256, BLS12-381, Pallas/Vesta
- **UCAN Integration**: Capability-based authorization tokens
- **DID Key Management**: Multi-chain wallet address derivation
## Repository Structure
```
sonr-io/crypto/
├── core/ # Core cryptographic primitives
│ ├── curves/ # Elliptic curve implementations
│ │ ├── native/ # Native curve arithmetic
│ │ │ ├── bls12381/ # BLS12-381 pairing-friendly curve
│ │ │ ├── k256/ # Secp256k1 (Bitcoin/Ethereum)
│ │ │ ├── p256/ # NIST P-256
│ │ │ └── pasta/ # Pallas/Vesta for ZK proofs
│ │ ├── bls12377_curve.go
│ │ ├── bls12381_curve.go
│ │ ├── ed25519_curve.go
│ │ ├── k256_curve.go
│ │ ├── p256_curve.go
│ │ └── pallas_curve.go
│ ├── protocol/ # MPC protocol framework
│ ├── commit.go # Pedersen commitments
│ ├── hash.go # Cryptographic hash utilities
│ └── mod.go # Modular arithmetic
├── mpc/ # Multi-Party Computation
│ ├── enclave.go # MPC enclave management
│ ├── protocol.go # DKG and signing protocols
│ ├── codec.go # Serialization/deserialization
│ ├── import.go # Enclave import/export
│ ├── verify.go # Signature verification
│ └── spec/ # UCAN/JWT specifications
├── tecdsa/ # Threshold ECDSA
│ └── dklsv1/ # 2-party ECDSA (DKLS v1)
│ ├── dkg/ # Distributed key generation
│ ├── sign/ # Threshold signing
│ ├── refresh/ # Key refresh protocol
│ └── dealer/ # Trusted dealer mode
├── ted25519/ # Threshold Ed25519
│ ├── frost/ # FROST protocol (DKG + signing)
│ └── ted25519/ # Core threshold Ed25519
├── signatures/ # Digital signature schemes
│ ├── bls/ # BLS signatures
│ │ └── bls_sig/ # Aggregatable BLS
│ ├── bbs/ # BBS+ selective disclosure
│ ├── schnorr/ # Schnorr variants
│ │ ├── mina/ # Mina protocol integration
│ │ └── nem/ # NEM blockchain support
│ └── common/ # Shared signature utilities
├── sharing/ # Secret sharing schemes
│ ├── shamir.go # Shamir's Secret Sharing
│ ├── feldman.go # Feldman VSS
│ ├── pedersen.go # Pedersen VSS
│ └── v1/ # Version 1 implementations
├── dkg/ # Distributed Key Generation
│ ├── frost/ # FROST DKG for Ed25519
│ ├── gennaro/ # Gennaro DKG protocol
│ └── gennaro2p/ # 2-party simplified DKG
├── bulletproof/ # Bulletproofs (range proofs)
│ ├── range_prover.go # Range proof generation
│ ├── range_verifier.go # Range proof verification
│ ├── ipp_prover.go # Inner product argument
│ └── generators.go # Generator points
├── accumulator/ # Cryptographic accumulators
│ ├── accumulator.go # RSA accumulator
│ ├── witness.go # Membership witnesses
│ └── proof.go # Inclusion/exclusion proofs
├── paillier/ # Paillier homomorphic encryption
│ ├── paillier.go # Public/private key operations
│ └── psf.go # Proof of safe factorization
├── ot/ # Oblivious Transfer
│ ├── base/simplest/ # Simplest OT protocol
│ └── extension/kos/ # KOS OT extension
├── zkp/ # Zero-Knowledge Proofs
│ └── schnorr/ # Schnorr proofs of knowledge
├── ucan/ # User-Controlled Authorization Networks
│ ├── capability.go # Capability management
│ ├── crypto.go # UCAN cryptographic operations
│ ├── jwt.go # JWT-based UCAN tokens
│ ├── verifier.go # Delegation chain verification
│ └── vault.go # Vault-specific capabilities
├── keys/ # Key management utilities
│ ├── didkey.go # DID key format support
│ ├── pubkey.go # Public key operations
│ └── parsers/ # Multi-chain key parsers
│ ├── btc_parser.go # Bitcoin key parsing
│ ├── eth_parser.go # Ethereum key parsing
│ ├── cosmos_parser.go # Cosmos SDK parsing
│ ├── sol_parser.go # Solana key parsing
│ └── ... # Other blockchain parsers
├── aead/ # Authenticated encryption
│ └── aes_gcm.go # AES-GCM AEAD
├── daed/ # Deterministic AEAD
│ └── aes_siv.go # AES-SIV encryption
├── ecies/ # Elliptic Curve IES
│ ├── encrypt.go # ECIES encryption
│ └── keys.go # Key generation
├── argon2/ # Password hashing
│ └── kdf.go # Argon2 key derivation
├── vrf/ # Verifiable Random Functions
│ └── vrf.go # Curve25519 VRF
├── ecdsa/ # ECDSA utilities
│ ├── canonical.go # Canonical signature encoding
│ └── deterministic.go # RFC 6979 deterministic signing
├── subtle/ # Low-level crypto utilities
│ ├── hkdf.go # HKDF key derivation
│ ├── random/ # Secure randomness
│ └── x25519.go # X25519 key exchange
└── internal/ # Internal utilities
├── ed25519/ # Extended Ed25519 operations
├── hash.go # Hash utilities
└── point.go # Point operations
```
## Core Modules
### 1. Multi-Party Computation (`mpc/`)
**Purpose**: Secure distributed key generation and threshold signing without trusted dealers.
#### MPC Enclave Structure
```go
type EnclaveData struct {
PubHex string `json:"pub_hex"` // Compressed public key (hex)
PubBytes []byte `json:"pub_bytes"` // Uncompressed public key
ValShare Message `json:"val_share"` // Alice (validator) keyshare
UserShare Message `json:"user_share"`// Bob (user) keyshare
Nonce []byte `json:"nonce"` // Encryption nonce
Curve CurveName `json:"curve"` // Elliptic curve name
}
```
#### Key Functions
```go
// Generate new MPC enclave (2-of-2 threshold)
func NewEnclave() (Enclave, error)
// Import enclave from various sources
func ImportEnclave(options ...ImportOption) (Enclave, error)
// Execute distributed signing protocol
func ExecuteSigning(signFuncVal SignFunc, signFuncUser SignFunc) ([]byte, error)
// Execute keyshare refresh protocol
func ExecuteRefresh(refreshFuncVal RefreshFunc, refreshFuncUser RefreshFunc,
curve CurveName) (Enclave, error)
// Verify signature with public key
func VerifyWithPubKey(pubKeyCompressed []byte, data []byte, sig []byte) (bool, error)
```
#### Security Features
- **2-of-2 Threshold**: Both parties required for signing
- **No Single Point of Failure**: Neither party can sign alone
- **Proactive Refresh**: Key rotation without changing public key
- **AES-GCM Encryption**: Secure enclave data encryption
- **SHA3-256 Hashing**: Cryptographic hash operations
#### Supported Curves
- `K256` - Secp256k1 (Bitcoin, Ethereum)
- `P256` - NIST P-256
- `ED25519` - Twisted Edwards curve
- `BLS12381` - Pairing-friendly curve
### 2. Elliptic Curves (`core/curves/`)
**Purpose**: Comprehensive elliptic curve implementations with unified interfaces.
#### Supported Curves
**Ed25519**
- Twisted Edwards curve for EdDSA signatures
- High-performance, constant-time operations
- Used in: Cosmos SDK, Solana, many modern systems
**Secp256k1 (K256)**
- Bitcoin and Ethereum standard curve
- ECDSA signature support
- Native field arithmetic implementations
**P-256 (Secp256r1)**
- NIST standard curve
- FIPS 186-4 compliant
- Wide hardware acceleration support
**BLS12-381**
- Pairing-friendly curve for BLS signatures
- Optimal ate pairing support
- Signature aggregation capabilities
- G1, G2, and GT group operations
**BLS12-377**
- Alternative pairing curve
- Used in certain ZK-SNARK constructions
**Pallas/Vesta**
- Pasta curves for recursive ZK proofs
- Cycle of curves for composition
#### Curve Interface
```go
type Curve interface {
Scalar
Point
Name() string
NewIdentityPoint() Point
NewGeneratorPoint() Point
Hash(input []byte) Point
// ... additional methods
}
```
### 3. Signature Schemes (`signatures/`)
#### BLS Signatures (`signatures/bls/`)
**Features:**
- Signature aggregation (combine multiple signatures)
- Threshold signatures (t-of-n)
- Multi-signatures with proof of possession
- Both G1 and G2 variants (tiny_bls and usual_bls)
**Key Operations:**
```go
// Sign message
func (sk *SecretKey) Sign(msg []byte) *Signature
// Aggregate multiple signatures
func AggregateSignatures(sigs ...*Signature) (*MultiSignature, error)
// Verify aggregated signature
func (sig *Signature) AggregateVerify(pks []*PublicKey, msgs [][]byte) (bool, error)
// Threshold key generation
func ThresholdGenerateKeys(threshold, total int) (*PublicKey, []*SecretKeyShare, error)
```
#### BBS+ Signatures (`signatures/bbs/`)
**Purpose**: Privacy-preserving signatures with selective disclosure
**Features:**
- Blind signatures for credential issuance
- Selective disclosure of attributes
- Zero-knowledge proofs of possession
- Unlinkable presentations
**Use Cases:**
- Verifiable credentials
- Anonymous authentication
- Privacy-preserving identity systems
#### Schnorr Signatures (`signatures/schnorr/`)
**Mina Protocol** (`mina/`):
- Poseidon hash function
- Schnorr signatures for Mina blockchain
- Challenge derivation
**NEM/Symbol** (`nem/`):
- Ed25519-Keccak variant
- NEM blockchain compatibility
### 4. Secret Sharing (`sharing/`)
#### Shamir's Secret Sharing
```go
type Shamir struct {
Threshold int
Limit int
Curve Curve
}
// Split secret into shares
func (s *Shamir) Split(secret []byte) ([]*ShamirShare, error)
// Reconstruct secret from shares
func (s *Shamir) Combine(shares []*ShamirShare) ([]byte, error)
```
#### Feldman Verifiable Secret Sharing
**Added Security**: Public commitments for share verification
```go
type FeldmanVerifier struct {
Commitments []curves.Point
}
// Verify share validity
func (v *FeldmanVerifier) Verify(share *ShamirShare) error
```
#### Pedersen Verifiable Secret Sharing
**Enhanced Privacy**: Computationally binding commitments
```go
// Split with verifiable commitments
func (p *Pedersen) Split(secret []byte) (*PedersenResult, error)
```
### 5. Threshold Cryptography
#### TECDSA (`tecdsa/dklsv1/`)
**Protocol**: Two-party threshold ECDSA (DKLS v1)
**Components:**
- **DKG**: Distributed key generation without trusted dealer
- **Signing**: Threshold signature generation
- **Refresh**: Proactive keyshare rotation
- **Dealer**: Optional trusted dealer mode
**Key Features:**
- No trusted third party required
- Active security against malicious adversaries
- Compatible with standard ECDSA verification
#### TED25519 (`ted25519/`)
**FROST Protocol** (`frost/`):
- Flexible Round-Optimized Schnorr Threshold signatures
- Efficient threshold Ed25519 signatures
- Three-round signing protocol
**Core Operations** (`ted25519/`):
```go
// Threshold key generation
func KeyGen(threshold, total int) ([]*SecretKeyShare, *PublicKey, error)
// Partial signature generation
func ThresholdSign(expandedSecretKeyShare []byte, publicKey []byte,
rShare []byte, R []byte, message []byte) []byte
// Signature aggregation
func AggregateSignatures(partialSigs [][]byte, R []byte) ([]byte, error)
```
### 6. Distributed Key Generation (`dkg/`)
#### Gennaro DKG (`dkg/gennaro/`)
**Standard DKG protocol** with:
- Four-round protocol
- Pedersen commitments
- Complaint handling
- Byzantine fault tolerance
#### FROST DKG (`dkg/frost/`)
**Optimized for Ed25519**:
- Two-round DKG
- Simplified complaint phase
- Integration with FROST signing
#### 2-Party DKG (`dkg/gennaro2p/`)
**Simplified protocol** for two parties:
- Reduced communication overhead
- Faster execution
- Suitable for client-server architectures
### 7. Zero-Knowledge Proofs
#### Bulletproofs (`bulletproof/`)
**Range Proofs without Trusted Setup**:
```go
// Prove value in range [0, 2^n]
func (p *RangeProver) Prove(v *big.Int, n int) (*RangeProof, error)
// Verify range proof
func (v *RangeVerifier) Verify(proof *RangeProof, commitment Point, n int) (bool, error)
// Batched range proofs (aggregate multiple proofs)
func BatchProve(values []*big.Int, n int) (*RangeProof, error)
```
**Features:**
- Logarithmic proof size: O(log n)
- Inner product arguments
- Batch verification support
- No trusted setup required
**Applications:**
- Confidential transactions
- Private smart contracts
- Privacy-preserving audits
#### Schnorr Proofs (`zkp/schnorr/`)
**Proof of Knowledge**:
- Discrete logarithm proofs
- Commitment proofs
- Non-interactive via Fiat-Shamir
### 8. Advanced Cryptography
#### Cryptographic Accumulators (`accumulator/`)
**RSA Accumulator** for set membership:
```go
// Add element to accumulator
func (acc *Accumulator) Add(element []byte) (*Witness, error)
// Generate membership proof
func (w *Witness) GenerateProof() (*Proof, error)
// Verify membership
func (acc *Accumulator) Verify(element []byte, proof *Proof) bool
```
**Use Cases:**
- Revocation lists
- Anonymous credentials
- Blockchain state commitments
#### Paillier Encryption (`paillier/`)
**Homomorphic Properties**:
- Additive homomorphism: E(m1) * E(m2) = E(m1 + m2)
- Scalar multiplication: E(m)^k = E(k * m)
- Threshold decryption support
**Applications:**
- Private computation
- Secure multi-party computation
- E-voting systems
#### Oblivious Transfer (`ot/`)
**Simplest OT** (`base/simplest/`):
- 1-out-of-2 OT protocol
- Based on Curve25519
**KOS Extension** (`extension/kos/`):
- Extend base OT to many OTs
- Efficient batch operations
- Correlated randomness generation
**Applications:**
- Private set intersection
- Secure two-party computation
- Password-authenticated key exchange
### 9. UCAN (User-Controlled Authorization Networks) (`ucan/`)
**Capability-Based Authorization**:
```go
// Create UCAN token
func CreateUCAN(issuer DID, audience DID, capabilities []Capability) (string, error)
// Attenuate capabilities (reduce permissions)
func AttenuateUCAN(parentToken string, newCapabilities []Capability) (string, error)
// Verify delegation chain
func VerifyDelegationChain(tokenString string, rootDID string) error
```
**Capability Types**:
- DID capabilities (read, write, update)
- DWN capabilities (records, protocols)
- Vault capabilities (sign, decrypt)
- DEX capabilities (swap, provide liquidity)
**Features:**
- JWT-based tokens
- Delegation chains
- Capability attenuation
- Proof-of-possession
- Expiration and not-before timestamps
### 10. Key Management (`keys/`)
#### DID Key Support (`didkey.go`)
```go
// Create DID from public key
func NewDID(publicKey []byte, keyType crypto.KeyType) (*DID, error)
// Derive blockchain address from DID
func (did *DID) Address() (string, error)
// Get raw public key bytes
func (did *DID) Raw() ([]byte, error)
```
#### Multi-Chain Parsers (`parsers/`)
**Supported Blockchains**:
- Bitcoin (BTC) - BIP32/BIP44 derivation
- Ethereum (ETH) - Keccak addresses
- Cosmos SDK - Bech32 encoding
- Solana (SOL) - Ed25519 keys
- Filecoin (FIL) - Secp256k1 keys
- TON - Ed25519 keys
### 11. Encryption Utilities
#### AEAD (`aead/`)
**AES-GCM Authenticated Encryption**:
```go
const (
KeySize = 32 // 256-bit key
NonceSize = 12 // 96-bit nonce
TagSize = 16 // 128-bit auth tag
)
// Encrypt with automatic nonce generation
func (c *AESGCMCipher) Encrypt(plaintext, aad []byte) ([]byte, error)
// Decrypt and verify
func (c *AESGCMCipher) Decrypt(ciphertext, aad []byte) ([]byte, error)
```
#### DAED (`daed/`)
**Deterministic AES-SIV**:
- Same plaintext → same ciphertext
- Useful for encrypted indices
- Misuse-resistant
#### ECIES (`ecies/`)
**Elliptic Curve Integrated Encryption Scheme**:
```go
// Generate ECIES keypair
func GenerateKey(curve Curve) (*PrivateKey, error)
// Encrypt message to public key
func Encrypt(recipientPubKey *PublicKey, message []byte) ([]byte, error)
// Decrypt with private key
func (sk *PrivateKey) Decrypt(ciphertext []byte) ([]byte, error)
```
### 12. Utility Modules
#### VRF (`vrf/`)
**Verifiable Random Function (Curve25519)**:
```go
// Generate VRF output and proof
func (sk *PrivateKey) Prove(message []byte) (vrf []byte, proof []byte)
// Verify VRF proof
func (pk *PublicKey) Verify(message, vrf, proof []byte) bool
```
**Applications:**
- Leader election
- Lottery systems
- Randomness beacons
- Sortition algorithms
#### Argon2 (`argon2/`)
**Password-Based Key Derivation**:
```go
// Derive key from password
func DeriveKey(password, salt []byte, keyLen uint32) []byte
```
**Parameters**:
- Time cost: 1 iteration (configurable)
- Memory cost: 64 MB (configurable)
- Parallelism: 4 threads (configurable)
#### ECDSA Utilities (`ecdsa/`)
**Canonical Encoding**:
- BIP 66 / RFC 6979 compliance
- Deterministic signature generation
- Low-S normalization
## Integration Patterns
### Usage in Sonr Blockchain
The crypto library is heavily integrated throughout the Sonr ecosystem:
#### DID Module
```go
import "github.com/sonr-io/crypto/keys"
import "github.com/sonr-io/crypto/mpc"
// DID creation from MPC enclave
enclave, _ := mpc.NewEnclave()
pubKey := enclave.GetPubPoint()
did := keys.NewDID(pubKey.Bytes(), crypto.Secp256k1)
```
#### DWN Module
```go
import "github.com/sonr-io/crypto/mpc"
import "github.com/sonr-io/crypto/aead"
// Vault operations
enclave := keeper.LoadEnclave(ctx, vaultID)
signature, _ := enclave.Sign(message)
// Encrypted data storage
cipher := aead.NewAESGCMCipher(key)
encrypted, _ := cipher.Encrypt(data, nil)
```
#### Service Module
```go
import "github.com/sonr-io/crypto/ucan"
// UCAN capability verification
verifier := ucan.NewVerifier(didResolver)
err := verifier.VerifyDelegationChain(ctx, tokenString)
```
### Motor Worker Integration
The WASM worker uses the crypto library extensively:
```go
import (
"github.com/sonr-io/crypto/mpc"
"github.com/sonr-io/crypto/core/curves"
)
//go:wasmexport sign
func sign() int32 {
// Load enclave from WASM memory
enclave := loadEnclave()
// Sign message
signature, _ := enclave.Sign(message)
return writeOutput(signature)
}
```
## Security Considerations
### Threat Model
The library is designed to protect against:
**Key Compromise**:
- MPC threshold schemes prevent single points of failure
- Proactive refresh rotates keyshares
**Insider Threats**:
- Multi-party protocols require cooperation
- No single party can perform operations alone
**Network Attacks**:
- Protocol messages are cryptographically protected
- Authentication prevents man-in-the-middle attacks
**Side-Channel Attacks**:
- Constant-time implementations where critical
- Secure memory handling
- Zeroization of sensitive data
### Best Practices
1. **Key Management**:
- Use hardware security modules when available
- Implement secure key backup and recovery
- Regular keyshare rotation via refresh protocols
2. **MPC Operations**:
- Secure communication channels (TLS)
- Proper authentication of parties
- Audit logging of all operations
3. **Random Number Generation**:
- Use `crypto/rand` for all random values
- Never reuse nonces in AEAD
- Verify randomness quality in production
4. **Error Handling**:
- Don't leak sensitive information in errors
- Validate all inputs
- Use constant-time comparisons for secrets
## Testing
The library includes comprehensive tests:
```bash
# Run all tests
go test ./...
# Run with race detection
go test -race ./...
# Generate coverage report
go test -cover ./...
# Run specific module tests
go test ./mpc/...
go test ./signatures/bls/...
go test ./bulletproof/...
# Benchmark performance
go test -bench=. ./core/curves/...
```
### Test Coverage
- **MPC**: Enclave operations, protocol execution, refresh
- **Signatures**: BLS aggregation, BBS+ proofs, Schnorr
- **Secret Sharing**: Shamir, Feldman, Pedersen
- **Threshold Crypto**: TECDSA, TED25519, DKG protocols
- **ZK Proofs**: Bulletproofs range proofs, Schnorr proofs
- **Encryption**: AEAD, ECIES, Paillier
- **Curves**: All curve operations, point arithmetic
## Dependencies
### External Libraries
```go
require (
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // Bitcoin crypto
github.com/consensys/gnark-crypto v0.19.0 // BLS12-377/381
golang.org/x/crypto v0.42.0 // Standard crypto
github.com/golang-jwt/jwt/v5 v5.3.0 // JWT tokens
)
```
### Internal Dependencies
The crypto library is **self-contained** and has no dependencies on other Sonr modules, making it suitable for independent use.
## Performance Characteristics
### Benchmarks (on AMD64, 2.5 GHz)
**Elliptic Curve Operations**:
- K256 scalar multiplication: ~50 µs
- Ed25519 signing: ~25 µs
- BLS12-381 pairing: ~1.2 ms
**MPC Operations**:
- DKG (2-party): ~15 ms
- Threshold signing: ~10 ms
- Key refresh: ~12 ms
**Signature Schemes**:
- BLS aggregation (100 sigs): ~150 ms
- BBS+ proof generation: ~80 ms
- Schnorr signing: ~30 µs
**Zero-Knowledge Proofs**:
- Bulletproof (64-bit range): ~40 ms
- Verification: ~25 ms
## Migration Checklist
When using the crypto library in a new project:
- [ ] Add dependency: `go get github.com/sonr-io/crypto@v1.0.1`
- [ ] Import required modules
- [ ] Initialize curve instances as needed
- [ ] Set up secure random number generation
- [ ] Implement proper error handling
- [ ] Add comprehensive tests
- [ ] Review security best practices
- [ ] Benchmark critical operations
- [ ] Set up monitoring/logging
- [ ] Document cryptographic assumptions
## Version Compatibility
**Go Version**: 1.24.4+
**Cosmos SDK**: Compatible with v0.50.x (if using Cosmos integration)
**Semantic Versioning**: The library follows semver
- Major: Breaking API changes
- Minor: New features, backwards compatible
- Patch: Bug fixes
## Related Documentation
- [Cosmos SDK Cryptography](https://docs.cosmos.network/main/learn/advanced/crypto)
- [BLS Signatures Spec](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature)
- [FROST Paper](https://eprint.iacr.org/2020/852)
- [Bulletproofs Paper](https://eprint.iacr.org/2017/1066)
- [UCAN Spec](https://github.com/ucan-wg/spec)
- [W3C DID Core](https://www.w3.org/TR/did-core/)
## Support
**Repository**: https://github.com/sonr-io/crypto
**Issues**: https://github.com/sonr-io/crypto/issues
**License**: Apache 2.0
-435
View File
@@ -1,435 +0,0 @@
# Highway (hway) Migration Context
> **Repository Migration**: `sonr-io/sonr` → `sonr-io/hway`
> **Components Moved**: `cmd/hway/`, `bridge/`, `internal/migrations/`
## Overview
Highway is a high-performance, PostgreSQL-backed HTTP service that handles OAuth2/OIDC authentication, WebAuthn flows, and asynchronous vault operations for the Sonr blockchain ecosystem. It serves as the authentication and task processing layer between clients and the Sonr blockchain.
## Architecture
### Core Technology Stack
- **Go**: 1.24.4
- **Task Queue**: Asynq (Redis-backed distributed task queue)
- **Actor System**: Proto.Actor for concurrency management
- **Database**: PostgreSQL with database/sql
- **Web Framework**: Echo v4
- **Authentication**: OAuth2, OIDC, WebAuthn, SIOP
- **Cryptography**: UCAN tokens, JWT signing (RS256)
### Service Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Highway Service (hway) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Bridge │ │ Tasks │ │ Handlers │ │
│ │ (HTTP) │──│ (Asynq) │──│ (Auth) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Proto.Actor System │ │
│ │ (Vault Actor Management) │ │
│ └─────────────────────────────────────────┘ │
│ │
└───────────┬───────────────────────┬─────────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│PostgreSQL│ │ Redis │
│ (State) │ │ (Queue) │
└──────────┘ └──────────┘
```
## Component Breakdown
### 1. Bridge Module (`bridge/`)
**Purpose**: HTTP API layer providing authentication and authorization services
**Key Files**:
- `bridge.go` - Main bridge server initialization
- `config.go` - Configuration management
- `queue.go` - Asynq task queue setup and management
**Handlers** (`bridge/handlers/`):
#### Authentication & Authorization
- `auth.go` - General authentication handlers
- `oidc.go` - OpenID Connect provider implementation
- Discovery endpoint (`.well-known/openid-configuration`)
- Authorization endpoint with PKCE support
- Token endpoint with JWT generation
- UserInfo endpoint
- JWKS endpoint for key rotation
- `siop.go` - Self-Issued OpenID Provider (SIOP) flows
- DID-based authentication
- Verifiable presentation handling
- `webauthn.go` - WebAuthn registration and authentication
- Challenge generation
- Credential verification
- Device binding
#### OAuth2 Implementation
- `oauth2_provider.go` - Core OAuth2 provider
- Authorization code flow
- Client credentials flow
- Refresh token flow
- Token introspection
- Token revocation
- `oauth2_register.go` - Dynamic client registration (RFC 7591)
- `oauth2_clients.go` - Client management and validation
- `oauth2_delegation.go` - Token delegation flows
- `oauth2_token_exchange.go` - Token exchange (RFC 8693)
- `oauth2_scopes.go` - Scope validation and management
- `oauth2_security.go` - Security utilities (PKCE, rate limiting)
- `oauth2_types.go` - OAuth2 type definitions
#### Vault Operations
- `vault.go` - Vault operation handlers
- Generate vault enclaves
- Sign with vault
- Verify signatures
- Import/Export to IPFS
- Refresh vault state
#### Utility Handlers
- `broadcast.go` - Transaction broadcasting
- `health.go` - Health check endpoints
- `websocket.go` - WebSocket connection management
- `types.go` - Shared type definitions
### 2. Task Processing (`bridge/tasks/`)
**Purpose**: Asynchronous task definitions and processing
**Key Files**:
- `types.go` - Task type constants and definitions
- `generate.go` - Vault generation tasks
- `signing.go` - Signing operation tasks
- `attenuation.go` - UCAN token attenuation tasks
**Task Types**:
```go
const (
TypeVaultGenerate = "vault:generate"
TypeVaultSign = "vault:sign"
TypeVaultRefresh = "vault:refresh"
TypeUCANAttenuation = "ucan:attenuation"
)
```
**Queue Configuration**:
```go
Queues: map[string]int{
"critical": 6, // High priority tasks
"default": 3, // Normal priority tasks
"low": 1, // Low priority tasks
}
```
### 3. Main Service (`cmd/hway/`)
**Purpose**: Service entry point and initialization
**Key Responsibilities**:
1. Initialize Asynq server with Redis connection
2. Configure worker pools and queue priorities
3. Register task handlers
4. Start HTTP server (Echo)
5. Setup signal handling for graceful shutdown
**Configuration**:
```go
const (
RedisAddr = "127.0.0.1:6379"
PostgresAddr = "127.0.0.1:5432"
HTTPPort = ":8090"
WorkerConcurrency = 10
)
```
### 4. Database Migrations (`internal/migrations/`)
**Purpose**: PostgreSQL schema management
**Migration Files**:
- `001_accounts_table.sql` - User account storage
- `002_credentials_table.sql` - WebAuthn credential storage
- `003_profiles_table.sql` - User profile data
- `004_vaults_table.sql` - Vault state persistence
- `005_create_cosmos_registry.sql` - Cosmos chain registry
- `006_execute_cosmos_registry.sql` - Registry functions
- `007_webauthn_to_vc_func.sql` - WebAuthn to VC conversion
- `008_create_coinpaprika_market_data.sql` - Market data tables
- `009_webauthn_options_functions.sql` - WebAuthn helper functions
- `010_crypto_asset_symbol_linking.sql` - Asset metadata
- `011_common_functions.sql` - Shared SQL functions
- `012_crypto_coin_price_data.sql` - Price data storage
- `013_add_asset_quality_filters.sql` - Asset filtering
- `014_sessions_table.sql` - Session management
## Integration Points
### With Sonr Blockchain (`snrd`)
- **RPC/REST API**: Queries blockchain state via Cosmos SDK endpoints
- **Transaction Broadcasting**: Submits signed transactions to chain
- **DID Resolution**: Resolves DIDs from blockchain state
- **Vault State**: Stores vault metadata on-chain
### With Motor/Worker (WASM Plugin)
- **Task Execution**: Highway enqueues tasks, Motor executes via WASM
- **Vault Operations**: Motor provides cryptographic operations
- **Enclave Management**: Actor system manages WASM plugin lifecycle
### With Client Applications
- **OAuth2/OIDC**: Standard OAuth2 authorization flows
- **WebAuthn**: Browser-based passwordless authentication
- **WebSocket**: Real-time task status updates
- **SSE**: Server-Sent Events for progress tracking
### With External Services
- **IPFS**: Vault backup/restore operations
- **Redis**: Distributed task queue and caching
- **PostgreSQL**: Persistent state storage
## Key Features
### 1. OAuth2/OIDC Provider
- Full OAuth2 authorization server implementation
- OpenID Connect provider with ID tokens
- Dynamic client registration (RFC 7591)
- Token exchange (RFC 8693)
- PKCE support for public clients
- Refresh token rotation
- Token revocation and introspection
### 2. WebAuthn Support
- FIDO2/WebAuthn registration flows
- Authentication with platform authenticators
- Credential lifecycle management
- Challenge-response validation
- Attestation verification
### 3. Vault Task Processing
- Asynchronous cryptographic operations
- Priority-based queue management
- Actor-based concurrency model
- Retry logic with exponential backoff
- Task status tracking and notifications
### 4. UCAN Token Management
- UCAN token generation and signing
- Capability delegation and attenuation
- Token chain verification
- Integration with DID system
## Security Considerations
### Authentication & Authorization
- Multi-factor authentication support
- JWT token signing with RS256
- PKCE for authorization code flow
- Origin validation for WebAuthn
- Rate limiting on all endpoints
### Data Protection
- Password hashing with Argon2
- Encrypted vault data in PostgreSQL
- Secure token generation (crypto/rand)
- HTTPS-only in production
- CORS configuration
### Vault Security
- WASM sandbox isolation for cryptographic operations
- No private key exposure to server
- Encrypted backup to IPFS
- Session timeout and auto-lock
## Configuration
### Environment Variables
```bash
# Service Configuration
HIGHWAY_PORT=8090
LOG_LEVEL=info
# Database
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=hway
# Redis
REDIS_URL=redis://localhost:6379
# IPFS
IPFS_API_URL=http://localhost:5001
# OAuth2/OIDC
OIDC_ISSUER=http://localhost:8090
JWT_SIGNING_KEY_PATH=/path/to/private-key.pem
JWT_PUBLIC_KEY_PATH=/path/to/public-key.pem
# Security
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3100
SESSION_SECRET=change-me-in-production
```
### Asynq Configuration
```go
asynq.Config{
Concurrency: 10,
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
StrictPriority: false,
ErrorHandler: asynq.ErrorHandlerFunc(handleError),
Logger: slog.Default(),
}
```
## API Endpoints
### Authentication
- `POST /auth/register` - User registration
- `POST /auth/login` - User login
- `POST /auth/logout` - User logout
- `POST /auth/refresh` - Refresh access token
### OAuth2/OIDC
- `GET /.well-known/openid-configuration` - OIDC discovery
- `GET /oauth2/authorize` - Authorization endpoint
- `POST /oauth2/token` - Token endpoint
- `GET /oauth2/userinfo` - User info endpoint
- `GET /oauth2/jwks` - JSON Web Key Set
- `POST /oauth2/register` - Dynamic client registration
- `POST /oauth2/revoke` - Token revocation
- `POST /oauth2/introspect` - Token introspection
### WebAuthn
- `POST /webauthn/register/begin` - Start registration
- `POST /webauthn/register/finish` - Complete registration
- `POST /webauthn/login/begin` - Start authentication
- `POST /webauthn/login/finish` - Complete authentication
### Vault Operations
- `POST /vault/generate` - Generate new vault
- `POST /vault/sign` - Sign with vault
- `POST /vault/verify` - Verify signature
- `POST /vault/refresh` - Refresh vault state
- `POST /vault/export` - Export to IPFS
- `POST /vault/import` - Import from IPFS
### WebSocket
- `WS /ws/tasks/{task_id}` - Task status updates
### Health & Monitoring
- `GET /health` - Health check
- `GET /health/ready` - Readiness probe
- `GET /health/live` - Liveness probe
## Testing Strategy
### Unit Tests
```bash
go test ./bridge/...
go test ./bridge/handlers/...
go test ./bridge/tasks/...
```
### Integration Tests
```bash
# Requires PostgreSQL and Redis
INTEGRATION=true go test ./...
```
### E2E Tests
```bash
# Requires full stack (PostgreSQL, Redis, IPFS)
E2E=true go test ./e2e/...
```
## Dependencies
### Required Services
- PostgreSQL 14+
- Redis 7+
- IPFS node (for vault operations)
### Go Modules (Key Dependencies)
- `github.com/hibiken/asynq` - Distributed task queue
- `github.com/labstack/echo/v4` - HTTP framework
- `github.com/asynkron/protoactor-go` - Actor system
- `github.com/lib/pq` - PostgreSQL driver
- `github.com/go-webauthn/webauthn` - WebAuthn library
- `github.com/golang-jwt/jwt/v5` - JWT handling
- `github.com/redis/go-redis/v9` - Redis client
## Build & Deployment
### Build Commands
```bash
# Build binary
go build -o hway ./cmd/hway
# Build with specific tags
go build -tags production -o hway ./cmd/hway
# Build Docker image
docker build -t sonr-hway:latest .
```
### Docker Deployment
```yaml
services:
hway:
image: onsonr/hway:latest
environment:
POSTGRES_HOST: postgres
REDIS_URL: redis://redis:6379
depends_on:
- postgres
- redis
ports:
- "8090:8090"
```
## Migration Checklist
When setting up the new `sonr-io/hway` repository:
- [ ] Copy `cmd/hway/` directory
- [ ] Copy `bridge/` directory (all handlers and tasks)
- [ ] Copy `internal/migrations/` for database schema
- [ ] Update import paths from `github.com/sonr-io/sonr` to new repo
- [ ] Create standalone `go.mod` with required dependencies
- [ ] Setup CI/CD for independent releases
- [ ] Create Dockerfile for containerized deployment
- [ ] Document environment variables and configuration
- [ ] Add PostgreSQL and Redis setup instructions
- [ ] Create integration test suite with testcontainers
- [ ] Setup database migration tooling (e.g., golang-migrate)
- [ ] Configure monitoring and observability (Prometheus/Grafana)
- [ ] Document OAuth2 client registration process
- [ ] Create API documentation (OpenAPI/Swagger)
## Related Documentation
- OAuth2 RFC 6749: https://tools.ietf.org/html/rfc6749
- OpenID Connect Core: https://openid.net/specs/openid-connect-core-1_0.html
- Dynamic Client Registration RFC 7591: https://tools.ietf.org/html/rfc7591
- WebAuthn Spec: https://www.w3.org/TR/webauthn/
- Asynq Documentation: https://github.com/hibiken/asynq
- Proto.Actor: https://proto.actor/
-653
View File
@@ -1,653 +0,0 @@
# Motor (motr) Migration Context
> **Repository Migration**: `sonr-io/sonr` → `sonr-io/motr`
> **Components Moved**: `cmd/motr/`, `cmd/vault/`, `packages/`, `web/`
> **Note**: The `crypto/` library was moved to its own repository at `sonr-io/crypto`
## Overview
Motor (formerly "motr") is a multi-purpose WebAssembly service that provides:
1. **Worker**: WASM-based cryptographic vault operations (formerly "vault")
2. **Payment Gateway**: W3C Payment Handler API compliant payment processing
3. **TypeScript SDK**: Browser and Node.js client libraries for vault and payment operations
4. **Web Applications**: Authentication and dashboard web apps
The name "Motor" reflects its role as the execution engine powering secure operations in the Sonr ecosystem.
## Repository Structure
```
sonr-io/motr/
├── worker/ # WASM vault operations (Go → WASM)
│ ├── main.go # Entrypoint with WASM exports
│ ├── vault/ # Vault operation implementations
│ └── mpc/ # Multi-party computation
├── server/ # HTTP server mode (Go)
│ ├── main.go # HTTP/Payment Gateway server
│ ├── handlers/ # Payment & OIDC handlers
│ └── middleware/ # Security & rate limiting
├── packages/ # TypeScript SDK and libraries
│ ├── es/ # @motr/es - Core SDK
│ │ ├── client/ # Vault client
│ │ ├── worker/ # Service worker integration
│ │ ├── plugin/ # Plugin system
│ │ └── codec/ # Encoding/signing utilities
│ │
│ ├── sdk/ # @motr/sdk - High-level SDK
│ ├── ui/ # @motr/ui - UI components
│ └── com/ # @motr/com - Common utilities
└── web/ # Web applications
├── auth/ # Authentication app
└── dash/ # Dashboard app
```
## Dependencies
Motor depends on the **Sonr Cryptography Library** (`github.com/sonr-io/crypto`), which was moved to its own repository. See MIGRATE_CRYPTO.md for details on the crypto library.
## Component 1: Worker (WASM Vault)
**Technology**: Go 1.24.4 → WebAssembly via TinyGo
**Runtime**: Extism (WebAssembly plugin host)
**Purpose**: Secure cryptographic operations in sandboxed environment
### Architecture
```
┌──────────────────────────────────────────────┐
│ Host Application │
│ (Browser, Node.js, or Highway) │
└───────────────┬──────────────────────────────┘
┌───────────────┐
│ Extism │ ◄─── WASM Runtime
│ Runtime │
└───────┬───────┘
┌───────────────────────────────────────────────┐
│ worker.wasm (Motor Worker) │
├───────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Vault │ │ MPC │ │
│ │ Operations │ │ Enclave │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Crypto Primitives │ │
│ │ (Ed25519, ECDSA, BLS, etc.) │ │
│ └──────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────┘
```
### WASM Exports (Go Functions)
All exported functions follow the Extism PDK pattern:
#### Core Vault Operations
```go
//go:wasmexport generate
func generate() int32
// Creates new MPC enclave with key generation
// Input: GenerateRequest{id: string}
// Output: GenerateResponse{data: EnclaveData, public_key: []byte}
//go:wasmexport refresh
func refresh() int32
// Refreshes enclave cryptographic material
// Input: RefreshRequest{enclave: EnclaveData}
// Output: RefreshResponse{okay: bool, data: EnclaveData}
//go:wasmexport sign
func sign() int32
// Signs arbitrary message with vault key
// Input: SignRequest{message: []byte, enclave: EnclaveData}
// Output: SignResponse{signature: []byte}
//go:wasmexport verify
func verify() int32
// Verifies signature against public key
// Input: VerifyRequest{public_key: []byte, message: []byte, signature: []byte}
// Output: VerifyResponse{valid: bool}
```
#### Multi-Chain Transaction Signing
```go
//go:wasmexport sign_cosmos_transaction
func signCosmosTransaction() int32
// Signs Cosmos SDK transaction
// Input: CosmosSignRequest{chain_id, account_number, sequence, tx_bytes}
// Output: SignedTransaction{signature, signed_doc}
//go:wasmexport sign_evm_transaction
func signEvmTransaction() int32
// Signs Ethereum/EVM transaction
// Input: EVMSignRequest{chain_id, nonce, tx_data, gas_limit}
// Output: SignedTransaction{v, r, s, raw_tx}
//go:wasmexport sign_message
func signMessage() int32
// Signs arbitrary message (EIP-191/EIP-712)
// Input: MessageSignRequest{message, encoding_type}
// Output: MessageSignature{signature, recovery_id}
```
#### Vault Import/Export (IPFS)
```go
//go:wasmexport export
func export() int32
// Exports encrypted vault to IPFS
// Input: ExportRequest{enclave: EnclaveData, password: []byte}
// Output: ExportResponse{cid: string, success: bool}
//go:wasmexport import
func import() int32
// Imports encrypted vault from IPFS
// Input: ImportRequest{cid: string, password: []byte}
// Output: ImportResponse{enclave: EnclaveData, success: bool}
```
#### WebAuthn Integration
```go
//go:wasmexport create_vault_enclave
func createVaultEnclave() int32
// Creates vault with WebAuthn configuration
// Input: VaultConfig{vault_id, webauthn_enabled, auto_lock_timeout}
// Output: VaultEnclave{enclave_data, webauthn_credentials}
//go:wasmexport unlock_vault
func unlockVault() int32
// Unlocks vault with WebAuthn or password
// Input: UnlockRequest{vault_id, auth_method, credentials}
// Output: UnlockResponse{success, session_token}
//go:wasmexport lock_vault
func lockVault() int32
// Locks vault and clears sensitive data
// Input: LockRequest{vault_id}
// Output: LockResponse{success}
```
#### Health & Monitoring
```go
//go:wasmexport get_vault_health
func getVaultHealth() int32
// Returns vault health status
// Output: EnclaveHealth{vault_id, status, last_activity, key_rotation_due}
//go:wasmexport get_version
func getVersion() int32
// Returns worker version and capabilities
// Output: VersionInfo{version, supported_chains, features}
```
### MPC Enclave Structure
```go
type EnclaveData struct {
ID string `json:"id"`
PublicKey []byte `json:"public_key"`
PrivateKeyShare []byte `json:"private_key_share"` // Encrypted
Threshold uint32 `json:"threshold"`
Parties uint32 `json:"parties"`
ChainID string `json:"chain_id"`
CreatedAt int64 `json:"created_at"`
LastRefresh int64 `json:"last_refresh"`
Metadata map[string]string `json:"metadata"`
}
```
**Note**: Motor relies on the Sonr Cryptography Library (`github.com/sonr-io/crypto`) for all cryptographic operations. The crypto library provides comprehensive primitives including Ed25519, ECDSA, BLS signatures, MPC, threshold cryptography, and more. See `MIGRATE_CRYPTO.md` for the complete crypto library documentation.
### Build Configuration
```bash
# Build WASM module with TinyGo
tinygo build -o worker.wasm -target wasi \
-no-debug \
-opt 2 \
-scheduler none \
./worker/main.go
# Optimize with wasm-opt
wasm-opt -O3 -o worker.optimized.wasm worker.wasm
# Build with Extism toolchain
extism compile worker.wasm -o worker.plugin.wasm
```
### IPFS Integration
```go
// IPFS Configuration
const (
IPFSStorageEndpoint = "http://127.0.0.1:5001/api/v0/add"
IPFSRetrievalEndpoint = "http://127.0.0.1:5001/api/v0/cat"
IPFSGateway = "https://ipfs.did.run/ipfs/"
)
// Export vault to IPFS
func ExportToIPFS(enclave *EnclaveData, password []byte) (cid string, error) {
// 1. Serialize enclave data
// 2. Encrypt with AES-256-GCM using password
// 3. Upload to IPFS
// 4. Return content ID (CID)
}
```
## Component 2: Server (Payment Gateway)
**Technology**: Go 1.24.4 HTTP Server
**Framework**: `go-wasm-http-server` (can run as service worker or HTTP)
**Purpose**: Payment processing and OIDC authorization
### Features
#### W3C Payment Handler API
- Payment request processing
- Card validation (Luhn algorithm, CVV, expiry)
- PCI DSS compliant tokenization
- Transaction signing with HMAC-SHA256
- AES-256-GCM encryption for card data
- Refund processing
- Audit logging
#### OIDC Authorization Server
- Full OpenID Connect provider
- Discovery endpoint
- Authorization with PKCE
- Token endpoint (JWT generation)
- UserInfo endpoint
- JWKS endpoint
- Refresh tokens
#### Security
- Rate limiting (100 req/min per client)
- Origin validation
- Security headers (CSP, X-Frame-Options)
- CORS configuration
- Secure token generation
- Card number masking
### API Endpoints
```
POST /api/payment/process - Process payment
POST /api/payment/validate - Validate payment method
POST /api/payment/refund - Process refund
GET /.well-known/openid-configuration
GET /oauth2/authorize
POST /oauth2/token
GET /oauth2/userinfo
GET /oauth2/jwks
```
## Component 3: TypeScript SDK (`packages/`)
**Purpose**: Browser and Node.js integration for Motor services
### Package Structure
#### `@motr/es` (Core SDK)
**Client Module** (`client/`):
- `VaultClient`: Main vault operations client
- `VaultClientWithIPFS`: IPFS-enabled vault client
- RPC/REST API integration
- Transaction broadcasting
**Worker Module** (`worker/`):
- `MotorServiceWorkerManager`: Service worker lifecycle
- `MotorClient`: HTTP client for Motor API
- Payment gateway client
- OIDC client integration
**Plugin Module** (`plugin/`):
- Plugin loading and caching
- WASM module verification
- Enclave storage (IndexedDB, localStorage)
- IPFS pinning integration
**Codec Module** (`codec/`):
- Address encoding (Bech32, EIP-55)
- Key management
- Transaction signing
- Signature verification
- Message serialization
**Auth Module** (`auth/`):
- WebAuthn registration
- WebAuthn authentication
- Credential management
- Passkey integration
**Generated Protobufs** (`protobufs/`):
- Cosmos SDK types
- Sonr blockchain types
- IBC types
- CosmWasm types
#### `@motr/sdk` (High-Level SDK)
- Simplified API wrappers
- Common operation helpers
- Error handling utilities
- TypeScript type definitions
#### `@motr/ui` (UI Components)
- React components for vault operations
- WebAuthn UI flows
- Payment forms
- Dashboard widgets
#### `@motr/com` (Common Utilities)
- Validation helpers
- Formatting utilities
- Type definitions
- Constants
### Usage Examples
```typescript
// Initialize vault client
import { createVaultClient } from '@motr/es';
const client = await createVaultClient({
rpcUrl: 'http://localhost:26657',
restUrl: 'http://localhost:1317',
});
// Generate vault
const vault = await client.generate({ id: 'my-vault' });
// Sign message
const signature = await client.sign({
message: new Uint8Array([1, 2, 3]),
enclave: vault.data,
});
// Export to IPFS
const cid = await client.export({
enclave: vault.data,
password: new Uint8Array([/* password */]),
});
```
```typescript
// Service worker integration
import { registerMotorServiceWorker } from '@motr/es';
const registration = await registerMotorServiceWorker({
workerUrl: '/worker.js',
scope: '/motor',
});
// Use in browser
const plugin = await createMotorPlugin({
auto_register_worker: true,
prefer_service_worker: true,
});
await plugin.processPayment({
amount: 100.00,
currency: 'USD',
method: 'card',
});
```
## Component 4: Web Applications
### Authentication App (`web/auth/`)
**Framework**: Next.js 14+
**Purpose**: WebAuthn registration and OIDC flows
**Features**:
- Passkey registration UI
- Login flows
- Session management
- OIDC client implementation
- WebAuthn ceremony handling
**Tech Stack**:
- Next.js (App Router)
- React 18
- TailwindCSS
- Fumadocs (documentation)
- Sonr UI components
### Dashboard App (`web/dash/`)
**Framework**: Next.js 14+
**Purpose**: Vault management and blockchain interaction
**Features**:
- Vault creation and management
- Transaction signing UI
- DID document viewer
- DWN record browser
- Token management
- Network switcher
**Tech Stack**:
- Next.js (App Router)
- React 18
- TailwindCSS
- @motr/sdk for blockchain interaction
- Charts and visualizations
### Shared Configuration
Both apps use:
- `@motr/ui` for shared components
- `@motr/sdk` for blockchain operations
- Environment-based configuration
- SSR/SSG optimization
- Edge runtime compatibility
## Build & Development
### Worker (WASM)
```bash
# Build worker WASM
make worker
# Or with TinyGo directly
tinygo build -o worker.wasm -target wasi ./worker/main.go
```
### Server (Payment Gateway)
```bash
# Build server
go build -o motr-server ./server/main.go
# Run server
./motr-server --port 8080
```
### TypeScript SDK
```bash
# Install dependencies
pnpm install
# Build all packages
pnpm -r build
# Build specific package
pnpm --filter @motr/es build
# Run tests
pnpm test
# Generate from protobufs
cd packages/es
pnpm gen:protobufs
```
### Web Applications
```bash
# Development
pnpm --filter @motr/auth dev
pnpm --filter @motr/dash dev
# Build
pnpm --filter @motr/auth build
pnpm --filter @motr/dash build
# Production
pnpm --filter @motr/auth start
```
## Testing Strategy
### Go (Worker)
```bash
# Unit tests
go test ./worker/...
go test ./server/...
# With race detection
go test -race ./...
# Coverage
go test -cover ./...
```
### TypeScript (SDK/Apps)
```bash
# Unit tests
pnpm test
# E2E tests
pnpm test:e2e
# Type checking
pnpm typecheck
```
### Integration Tests
```bash
# Requires Motor server running
INTEGRATION=true pnpm test
```
## Configuration
### Worker Environment Variables
```bash
# WASM runtime configuration (via Extism)
CHAIN_ID=sonr-testnet-1
PASSWORD=default-password
IPFS_GATEWAY=https://ipfs.did.run/ipfs/
```
### SDK Configuration
```typescript
interface MotorConfig {
// RPC endpoints
rpcUrl: string;
restUrl: string;
// IPFS
ipfsGateways: string[];
enableIPFSPersistence: boolean;
// Service worker
workerUrl: string;
preferServiceWorker: boolean;
// Security
timeout: number;
maxRetries: number;
}
```
### Web App Environment Variables
```bash
# Common
NODE_ENV=production
NEXT_PUBLIC_CHAIN_ID=sonr-testnet-1
# Auth App
NEXT_PUBLIC_AUTH_URL=http://localhost:3100
NEXT_PUBLIC_WEBAUTHN_RP_ID=localhost
NEXT_PUBLIC_WEBAUTHN_RP_NAME="Sonr Auth"
# Dashboard App
NEXT_PUBLIC_RPC_ENDPOINT=http://localhost:26657
NEXT_PUBLIC_REST_ENDPOINT=http://localhost:1317
NEXT_PUBLIC_IPFS_GATEWAY=https://ipfs.io/ipfs/
```
## Migration Checklist
When setting up the new `sonr-io/motr` repository:
### Worker
- [ ] Copy `cmd/vault/``worker/`
- [ ] Update import paths to use `github.com/sonr-io/crypto`
- [ ] Create `worker/go.mod` with crypto dependency
- [ ] Setup TinyGo build scripts
- [ ] Add WASM optimization pipeline
- [ ] Document export functions and types
- [ ] Create test suite for WASM functions
### Server
- [ ] Copy `cmd/motr/``server/`
- [ ] Update payment gateway handlers
- [ ] Configure OIDC provider
- [ ] Setup rate limiting
- [ ] Add monitoring/metrics
- [ ] Create Dockerfile
- [ ] Document API endpoints
### TypeScript SDK
- [ ] Copy `packages/` directory
- [ ] Update package names to `@motr/*`
- [ ] Setup pnpm workspace
- [ ] Configure build pipeline (tsup/rollup)
- [ ] Setup Biome for linting/formatting
- [ ] Generate types from protobuf
- [ ] Create comprehensive tests
- [ ] Setup Changesets for versioning
- [ ] Publish to npm registry
### Web Applications
- [ ] Copy `web/` directory
- [ ] Update dependencies to use `@motr/*`
- [ ] Configure environment variables
- [ ] Setup build and deployment
- [ ] Create Docker images
- [ ] Add E2E tests
- [ ] Document user flows
### General
- [ ] Create monorepo structure
- [ ] Setup CI/CD pipelines
- [ ] Configure release automation
- [ ] Create API documentation
- [ ] Write migration guide
- [ ] Setup npm organization (@motr)
- [ ] Configure security scanning
- [ ] Setup monitoring and logging
## Related Documentation
- TinyGo: https://tinygo.org/
- Extism: https://extism.org/
- W3C Payment Handler API: https://www.w3.org/TR/payment-handler/
- WebAuthn: https://www.w3.org/TR/webauthn/
- IPFS: https://docs.ipfs.tech/
- Next.js: https://nextjs.org/
- pnpm Workspaces: https://pnpm.io/workspaces
-120
View File
@@ -1,120 +0,0 @@
#!/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
@@ -1,169 +0,0 @@
# 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
@@ -1,363 +0,0 @@
// 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
@@ -1,463 +0,0 @@
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
@@ -1,96 +0,0 @@
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
@@ -1,338 +0,0 @@
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
@@ -1,181 +0,0 @@
// 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
@@ -1,344 +0,0 @@
// 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
@@ -1,338 +0,0 @@
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
@@ -1,187 +0,0 @@
// 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
@@ -1,225 +0,0 @@
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/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/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
@@ -1,355 +0,0 @@
// 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
@@ -1,606 +0,0 @@
// 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
@@ -1,33 +0,0 @@
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
@@ -1,613 +0,0 @@
// 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
@@ -1,559 +0,0 @@
// 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
@@ -1,532 +0,0 @@
// 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
@@ -1,354 +0,0 @@
// 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
@@ -1,316 +0,0 @@
// 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
@@ -1,357 +0,0 @@
// 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
@@ -1,442 +0,0 @@
// 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
@@ -1,191 +0,0 @@
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
@@ -1,316 +0,0 @@
// 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
@@ -1,268 +0,0 @@
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)
}
+3 -20
View File
@@ -2,17 +2,11 @@
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/main/.schema/devbox.schema.json",
"packages": [
"gum@latest",
"nodejs@latest",
"docker@latest",
"pnpm@10.14.0",
"go@1.24.4",
"gcc@latest",
"tinygo@latest",
"templ@latest",
"jq@latest",
"trunk@latest",
"wrkflw@latest",
"uv@latest",
"commitizen@latest",
"mods@latest",
"yq@latest"
],
@@ -42,24 +36,13 @@
"alias pr-description='./scripts/github-env.sh pr-description'",
"alias validate-default-branch='./scripts/github-env.sh validate-default-branch'",
"alias commit-summary='./scripts/github-env.sh commit-summary'",
"sh ./scripts/devbox-env.sh install"
"go mod download"
],
"scripts": {
"build": "make build-all",
"test": "make test-all",
"snapshot": "make snapshot",
"release": "make release",
"build:client": "make build-client",
"build:core": "make -C cmd/snrd build",
"test:client": "make test-client",
"test:core": "make test-app",
"test:dex": "make test-module MODULE=dex",
"test:devops": "make test-devops",
"test:did": "make test-module MODULE=did",
"test:dwn": "make test-dwn-ci",
"test:svc": "make test-module MODULE=svc",
"snapshot:core": "make -C cmd/snrd snapshot",
"release:core": "make -C cmd/snrd release"
"release": "make release"
}
}
}
+116 -385
View File
@@ -1,70 +1,6 @@
{
"lockfile_version": "1",
"packages": {
"commitizen@latest": {
"last_modified": "2025-07-28T17:09:23Z",
"resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#commitizen",
"source": "devbox-search",
"version": "4.8.3",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/ngh28qryvmw4pfx6dqql0w1kp2fid137-python3.13-commitizen-4.8.3",
"default": true
},
{
"name": "dist",
"path": "/nix/store/qq9fg0j0g3a9s708rim0m4v3c2d9jhqc-python3.13-commitizen-4.8.3-dist"
}
],
"store_path": "/nix/store/ngh28qryvmw4pfx6dqql0w1kp2fid137-python3.13-commitizen-4.8.3"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/bnj75p83fzc91x558wpm1005gn5cy73p-python3.13-commitizen-4.8.3",
"default": true
},
{
"name": "dist",
"path": "/nix/store/m80fz0s37d5m5yfnbssffiwgv1g763gz-python3.13-commitizen-4.8.3-dist"
}
],
"store_path": "/nix/store/bnj75p83fzc91x558wpm1005gn5cy73p-python3.13-commitizen-4.8.3"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/rhrm23f9ldc3gpanz2pasjlsx1dzz8rf-python3.13-commitizen-4.8.3",
"default": true
},
{
"name": "dist",
"path": "/nix/store/d0zyh4j990sdngvaiy6rsb12533vh92p-python3.13-commitizen-4.8.3-dist"
}
],
"store_path": "/nix/store/rhrm23f9ldc3gpanz2pasjlsx1dzz8rf-python3.13-commitizen-4.8.3"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/hwx4wp8nl0kqas6lj5bwmj75p0hr0njf-python3.13-commitizen-4.8.3",
"default": true
},
{
"name": "dist",
"path": "/nix/store/84z1l6n7mvjsd8wida72an7sc5hb386f-python3.13-commitizen-4.8.3-dist"
}
],
"store_path": "/nix/store/hwx4wp8nl0kqas6lj5bwmj75p0hr0njf-python3.13-commitizen-4.8.3"
}
}
},
"docker@latest": {
"last_modified": "2025-08-05T11:35:34Z",
"resolved": "github:NixOS/nixpkgs/a683adc19ff5228af548c6539dbc3440509bfed3#docker",
@@ -297,6 +233,122 @@
}
}
},
"jq@latest": {
"last_modified": "2025-09-21T09:21:16Z",
"resolved": "github:NixOS/nixpkgs/a1f79a1770d05af18111fbbe2a3ab2c42c0f6cd0#jq",
"source": "devbox-search",
"version": "1.8.1",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/8cadjdrc1gjc66ijgn2wnmfj6yajnxmb-jq-1.8.1-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/11rdl4l7v5gswf5gdizk02jy39bw7bby-jq-1.8.1-man",
"default": true
},
{
"name": "out",
"path": "/nix/store/i8v8j94vx41c1wfzp1n69ddbrrf192pq-jq-1.8.1"
},
{
"name": "dev",
"path": "/nix/store/vcjkcdi02h123i073sxma5kfhps9z2fd-jq-1.8.1-dev"
},
{
"name": "doc",
"path": "/nix/store/sbr5199a3y3vpx1kaphyl4nnjlpzzzym-jq-1.8.1-doc"
}
],
"store_path": "/nix/store/8cadjdrc1gjc66ijgn2wnmfj6yajnxmb-jq-1.8.1-bin"
},
"aarch64-linux": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/ync89vi4g1xnkj4fxspc63zfvs7dk8aw-jq-1.8.1-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/44k1613ynml3cgx5v9k6saqw994hqcl9-jq-1.8.1-man",
"default": true
},
{
"name": "out",
"path": "/nix/store/hz4c9q3baf434zw2y0yzb3a5kkj3rhwl-jq-1.8.1"
},
{
"name": "dev",
"path": "/nix/store/194yri6cyqad6yvbhpqp5wswsppnsi7x-jq-1.8.1-dev"
},
{
"name": "doc",
"path": "/nix/store/cjmvz6ma3qnhdvapjf2yghw28lrah0ja-jq-1.8.1-doc"
}
],
"store_path": "/nix/store/ync89vi4g1xnkj4fxspc63zfvs7dk8aw-jq-1.8.1-bin"
},
"x86_64-darwin": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/8kcm1dcivx14cp5z39wzma81jbrvaazs-jq-1.8.1-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/88l2hgigy77vjgkdjps6l6vl8svxgaaa-jq-1.8.1-man",
"default": true
},
{
"name": "out",
"path": "/nix/store/ladmn1j7zikwdmih15d8rfh7z550y1q2-jq-1.8.1"
},
{
"name": "dev",
"path": "/nix/store/wjxxw199ix3mkg8ms6k0hqfg7swxr2qv-jq-1.8.1-dev"
},
{
"name": "doc",
"path": "/nix/store/17mfk267mjhnvgbm03w0scsfq82a65na-jq-1.8.1-doc"
}
],
"store_path": "/nix/store/8kcm1dcivx14cp5z39wzma81jbrvaazs-jq-1.8.1-bin"
},
"x86_64-linux": {
"outputs": [
{
"name": "bin",
"path": "/nix/store/7jvwwz45qap70i6asxzcnbshhz88a5sv-jq-1.8.1-bin",
"default": true
},
{
"name": "man",
"path": "/nix/store/laqc7c2c9j09vm1w58vmj633vc2wb3wv-jq-1.8.1-man",
"default": true
},
{
"name": "doc",
"path": "/nix/store/j572lmsgrx4a9bim6gwlyn9mnlf67490-jq-1.8.1-doc"
},
{
"name": "out",
"path": "/nix/store/byi56qh6h63rdhfjh4plnyndfcpqx13p-jq-1.8.1"
},
{
"name": "dev",
"path": "/nix/store/5rp4imr3xn3qwwxpgmss2q9pj8bak9wy-jq-1.8.1-dev"
}
],
"store_path": "/nix/store/7jvwwz45qap70i6asxzcnbshhz88a5sv-jq-1.8.1-bin"
}
}
},
"mods@latest": {
"last_modified": "2025-07-28T17:09:23Z",
"resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#mods",
@@ -345,231 +397,6 @@
}
}
},
"nodejs@latest": {
"last_modified": "2025-08-11T07:05:29Z",
"plugin_version": "0.0.2",
"resolved": "github:NixOS/nixpkgs/9585e9192aadc13ec3e49f33f8333bd3cda524df#nodejs_24",
"source": "devbox-search",
"version": "24.5.0",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/b1j05q96hwagn787p2jlgqcjg2nf5x49-nodejs-24.5.0",
"default": true
},
{
"name": "dev",
"path": "/nix/store/j6ayg4xpqy9xdxgrhpqylzq8v7v07c6r-nodejs-24.5.0-dev"
},
{
"name": "libv8",
"path": "/nix/store/3ys6v5s5gvd9snwnl4saynl6av7mz3vy-nodejs-24.5.0-libv8"
}
],
"store_path": "/nix/store/b1j05q96hwagn787p2jlgqcjg2nf5x49-nodejs-24.5.0"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/1kn0vh4gf3a22arldrw694apq3fhgp15-nodejs-24.5.0",
"default": true
},
{
"name": "dev",
"path": "/nix/store/i3lqaj3j6znhnzh8ayka6q85r81ppxnw-nodejs-24.5.0-dev"
},
{
"name": "libv8",
"path": "/nix/store/jjw6xgmg6qynp336g9igqnzlfbhzxr2i-nodejs-24.5.0-libv8"
}
],
"store_path": "/nix/store/1kn0vh4gf3a22arldrw694apq3fhgp15-nodejs-24.5.0"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/sbcg21wp4bdzyh2542v77sp535kvfbfq-nodejs-24.5.0",
"default": true
},
{
"name": "dev",
"path": "/nix/store/fg7pi9s6m0spci1pfqbny0kxmk832i3r-nodejs-24.5.0-dev"
},
{
"name": "libv8",
"path": "/nix/store/75b7iix0pbmxmfnmv90l3q0ll1gc75az-nodejs-24.5.0-libv8"
}
],
"store_path": "/nix/store/sbcg21wp4bdzyh2542v77sp535kvfbfq-nodejs-24.5.0"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/357id3rjy9417k4dkvxxmpgd9bxrwc7l-nodejs-24.5.0",
"default": true
},
{
"name": "dev",
"path": "/nix/store/0drh8jjq84sji6889l2k3ysmvy7sc9sg-nodejs-24.5.0-dev"
},
{
"name": "libv8",
"path": "/nix/store/kdlv4q7sgap0z43cylklhxz1g1q7751b-nodejs-24.5.0-libv8"
}
],
"store_path": "/nix/store/357id3rjy9417k4dkvxxmpgd9bxrwc7l-nodejs-24.5.0"
}
}
},
"pnpm@10.14.0": {
"last_modified": "2025-08-02T16:19:54Z",
"resolved": "github:NixOS/nixpkgs/7b6929d8b900de3142638310f8bc40cff4f2c507#pnpm",
"source": "devbox-search",
"version": "10.14.0",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/zbrkyacg6llvr23i9jymcqpdply66wcb-pnpm-10.14.0",
"default": true
}
],
"store_path": "/nix/store/zbrkyacg6llvr23i9jymcqpdply66wcb-pnpm-10.14.0"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/cfpp6ffy67vvzz1l6vqddkxzffs2m790-pnpm-10.14.0",
"default": true
}
],
"store_path": "/nix/store/cfpp6ffy67vvzz1l6vqddkxzffs2m790-pnpm-10.14.0"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/iryjb73pn84bczcr751608rjbpbzm721-pnpm-10.14.0",
"default": true
}
],
"store_path": "/nix/store/iryjb73pn84bczcr751608rjbpbzm721-pnpm-10.14.0"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/q4cljfi7la5sbb75vb2kkgvbg2qwgz2x-pnpm-10.14.0",
"default": true
}
],
"store_path": "/nix/store/q4cljfi7la5sbb75vb2kkgvbg2qwgz2x-pnpm-10.14.0"
}
}
},
"templ@latest": {
"last_modified": "2025-08-01T06:30:45Z",
"resolved": "github:NixOS/nixpkgs/a7822a17050fedda63794775b9090880c8214290#templ",
"source": "devbox-search",
"version": "0.3.924",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/pncq34v68zaw43zss45adzs5j4iwris4-templ-0.3.924",
"default": true
}
],
"store_path": "/nix/store/pncq34v68zaw43zss45adzs5j4iwris4-templ-0.3.924"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/glvzrzimbk8i3886sh6wj7z6lxh7ab21-templ-0.3.924",
"default": true
}
],
"store_path": "/nix/store/glvzrzimbk8i3886sh6wj7z6lxh7ab21-templ-0.3.924"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/iqjim6gvr77fn2cflvd289m3l8sbpc8z-templ-0.3.924",
"default": true
}
],
"store_path": "/nix/store/iqjim6gvr77fn2cflvd289m3l8sbpc8z-templ-0.3.924"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/f42fpcgyisq0n2n4zp4wg54kwbsc5jqy-templ-0.3.924",
"default": true
}
],
"store_path": "/nix/store/f42fpcgyisq0n2n4zp4wg54kwbsc5jqy-templ-0.3.924"
}
}
},
"tinygo@latest": {
"last_modified": "2025-07-28T17:09:23Z",
"resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#tinygo",
"source": "devbox-search",
"version": "0.37.0",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/hbcc5b2p7ldmdgfbhjw0k7k8m3qmar32-tinygo-0.37.0",
"default": true
}
],
"store_path": "/nix/store/hbcc5b2p7ldmdgfbhjw0k7k8m3qmar32-tinygo-0.37.0"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/kh09k20p9p7m4wpb159v0ik33qgmz2ji-tinygo-0.37.0",
"default": true
}
],
"store_path": "/nix/store/kh09k20p9p7m4wpb159v0ik33qgmz2ji-tinygo-0.37.0"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/bsgaj2l6aj95bpagw6ny7ank55ahg7d5-tinygo-0.37.0",
"default": true
}
],
"store_path": "/nix/store/bsgaj2l6aj95bpagw6ny7ank55ahg7d5-tinygo-0.37.0"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/lrnd6d9r7km8mfybv2jfszaq7mkf7dz5-tinygo-0.37.0",
"default": true
}
],
"store_path": "/nix/store/lrnd6d9r7km8mfybv2jfszaq7mkf7dz5-tinygo-0.37.0"
}
}
},
"trunk@latest": {
"last_modified": "2025-07-28T17:09:23Z",
"resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#trunk",
@@ -618,102 +445,6 @@
}
}
},
"uv@latest": {
"last_modified": "2025-07-28T17:09:23Z",
"resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#uv",
"source": "devbox-search",
"version": "0.8.2",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/db9y1b002zlnyjgpsnbl9hvlwsiqajl4-uv-0.8.2",
"default": true
}
],
"store_path": "/nix/store/db9y1b002zlnyjgpsnbl9hvlwsiqajl4-uv-0.8.2"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/bdps6h2gn2rysavc3cq2slqnjlsyyk03-uv-0.8.2",
"default": true
}
],
"store_path": "/nix/store/bdps6h2gn2rysavc3cq2slqnjlsyyk03-uv-0.8.2"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/91g6383zq1wbiik2an7g6yfrj294c19v-uv-0.8.2",
"default": true
}
],
"store_path": "/nix/store/91g6383zq1wbiik2an7g6yfrj294c19v-uv-0.8.2"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/swq8qrr7n5gkc1b4940q62a3ll52prgl-uv-0.8.2",
"default": true
}
],
"store_path": "/nix/store/swq8qrr7n5gkc1b4940q62a3ll52prgl-uv-0.8.2"
}
}
},
"wrkflw@latest": {
"last_modified": "2025-08-08T08:05:48Z",
"resolved": "github:NixOS/nixpkgs/a3f3e3f2c983e957af6b07a1db98bafd1f87b7a1#wrkflw",
"source": "devbox-search",
"version": "0.4.0",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/i8502axxgj1diz3zi84mn82zn7cgy8z6-wrkflw-0.4.0",
"default": true
}
],
"store_path": "/nix/store/i8502axxgj1diz3zi84mn82zn7cgy8z6-wrkflw-0.4.0"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/i879qdphllj01iyzh0hhrqymgdgcviah-wrkflw-0.4.0",
"default": true
}
],
"store_path": "/nix/store/i879qdphllj01iyzh0hhrqymgdgcviah-wrkflw-0.4.0"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/hafx7sdxsd26g25ds6mxamncf9wmawhg-wrkflw-0.4.0",
"default": true
}
],
"store_path": "/nix/store/hafx7sdxsd26g25ds6mxamncf9wmawhg-wrkflw-0.4.0"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/04bqnq1lrymknypj6ppfp78qm51l3hfq-wrkflw-0.4.0",
"default": true
}
],
"store_path": "/nix/store/04bqnq1lrymknypj6ppfp78qm51l3hfq-wrkflw-0.4.0"
}
}
},
"yq@latest": {
"last_modified": "2025-09-18T16:33:27Z",
"resolved": "github:NixOS/nixpkgs/f4b140d5b253f5e2a1ff4e5506edbf8267724bde#yq",
-173
View File
@@ -1,173 +0,0 @@
---
title: Requesting Permissions
description: A guide to setting up and managing wallet connections with the Sonr decentralized identity system
icon: "shield-check"
---
Sonr provides a seamless and secure way for users to connect their wallets to decentralized applications. This guide covers the different methods for establishing and managing wallet connections, from simple browser-based interactions to backend service integrations.
## The Sonr Connection Model
Unlike traditional Web3 wallets that require browser extensions, Sonr uses a combination of WebAuthn and Decentralized Identifiers (DIDs) to create a secure, passwordless connection experience.
<CardGroup>
<Card title="User-Centric" href="/blockchain/modules/did/">
Users control their identity and grant permissions to applications, not the
other way around.
</Card>
<Card title="Passwordless" href="/blockchain/modules/did/">
WebAuthn enables biometric and security key authentication, eliminating the
need for seed phrases.
</Card>
<Card title="Multi-Device" href="/blockchain/modules/dwn/">
Users can securely access their Vault from any device with a modern web
browser.
</Card>
</CardGroup>
## Connecting in the Browser
For web applications, the Sonr SDK provides a simple way to initiate a wallet connection.
<Steps>
<Step>
### 1. Initialize the SDK
First, initialize the Sonr SDK in your application. For this example, we'll use the CDN version.
```html
<script type="module">
import { Sonr } from "https://cdn.jsdelivr.net/npm/@sonr/sdk";
const sonr = new Sonr({ httpUrl: "http://localhost:1317" });
</script>
```
</Step>
<Step>
### 2. Request Authentication
Use the `sonr.authenticate()` method to prompt the user to connect their wallet. This will trigger the browser's WebAuthn flow.
```javascript
async function connectWallet() {
try {
const session = await sonr.authenticate();
console.log("Wallet connected!", session);
// You now have a secure session with the user's Vault
} catch (error) {
console.error("Failed to connect wallet:", error);
}
}
```
</Step>
<Step>
### 3. Handle the Session
The `session` object returned from `authenticate()` contains the user's DID and a UCAN token with the requested permissions. You can use this session to interact with the user's Vault.
```javascript
// Example: Get the user's balance
const balance = await session.vault.getAccountBalance();
console.log("User balance:", balance);
```
</Step>
</Steps>
## Backend Wallet Connections
For backend services, you can use the Sonr SDK to interact with user Vaults on behalf of your application.
<Steps>
<Step>
### 1. Service Registration
Your backend service must be registered on the Sonr network. This provides your service with its own DID and allows it to request permissions from users.
{/* Service registration documentation is referenced but not yet available in the docs structure */}
</Step>
<Step>
### 2. Requesting Permissions
Your service can request permissions from users by generating a UCAN request. This is typically done through a user-facing application.
```typescript
// Example: Requesting permission to read a user's profile
const ucanRequest = await sonr.ucan.request({
audience: "did:sonr:your-service-did",
resource: `dwn://user-did/profile/read`,
});
// Present this request to the user to be signed by their Vault
```
</Step>
<Step>
### 3. Using Delegated Capabilities
Once a user has approved your request, you will receive a delegated UCAN token. You can use this token to perform actions on the user's behalf.
```go
// Example: Using a delegated UCAN in a Go backend
import "github.com/sonr-io/sonr/x/sonr/pkgs/sdk"
func GetUserProfile(userDID string, delegatedUcan string) (*Profile, error) {
sonr, _ := sdk.NewSonr(rpcEndpoint, "")
// Use the delegated UCAN to access the user's profile
profile, err := sonr.GetUserProfile(userDID, delegatedUcan)
if err != nil {
return nil, err
}
return profile, nil
}
```
</Step>
</Steps>
## Managing Connections
### Checking Connection Status
You can check the current connection status at any time:
```javascript
const session = await sonr.getSession();
if (session) {
console.log("User is connected:", session.did);
} else {
console.log("User is not connected.");
}
```
### Disconnecting
To disconnect a wallet, simply clear the session from your application's state:
```javascript
await sonr.logout();
console.log("User has been disconnected.");
```
This will revoke the current session's UCAN token, but it will not remove any permissions the user has granted to your service.
## Security Considerations
- **UCAN Scopes**: Always request the minimum permissions necessary for your application to function.
- **Token Storage**: Securely store delegated UCAN tokens on your backend. Never expose them on the client-side.
- **Revocation**: Your application should handle UCAN revocations gracefully.
## Next Steps
- [Sending Payments](/highway/wallets/sending-payments)
- [Understanding UCANs](/blockchain/modules/svc/ucan)
- [Explore DWN Architecture](/blockchain/modules/dwn/)
-233
View File
@@ -1,233 +0,0 @@
---
title: "Motor WASM Service Worker Usage"
description: "Comprehensive guide to using the Motor WASM service worker for secure browser-based DWN and Wallet APIs"
icon: "microchip"
sidebarTitle: "Motor WASM"
---
<Info>
Motor is a WebAssembly service worker providing secure, client-side cryptographic operations and decentralized web node (DWN) capabilities.
</Info>
## Overview
Motor is a WebAssembly-powered service worker that enables:
- Secure client-side cryptographic operations
- Decentralized Web Node (DWN) APIs
- Cross-platform support for browsers and Node.js
<CardGroup cols={2}>
<Card title="DWN API" icon="database">
Create, read, update, and delete records with optional encryption
</Card>
<Card title="Wallet API" icon="wallet" color="#22863a">
UCAN token generation, digital signatures, and verification
</Card>
</CardGroup>
## Prerequisites
<Check>
- Node.js 20+ and pnpm
- Browser with Service Worker support
- HTTP/HTTPS server for WASM files
</Check>
## Installation
<CodeGroup>
```bash npm
npm install @sonr.io/es
```
```bash pnpm
pnpm add @sonr.io/es
```
```bash yarn
yarn add @sonr.io/es
```
</CodeGroup>
## Usage Examples
### Basic Initialization
<Tabs>
<Tab title="Browser Auto-Detection">
```typescript
import { createMotorPlugin } from '@sonr.io/es/client/motor';
// Automatically detects browser vs Node.js environment
const plugin = await createMotorPlugin();
// Get issuer DID
const issuer = await plugin.getIssuerDID();
console.log('Issuer DID:', issuer.issuer_did);
```
</Tab>
<Tab title="Browser Service Worker">
```typescript
import { createMotorPluginForBrowser } from '@sonr.io/es/client/motor';
const plugin = await createMotorPluginForBrowser('/motor-worker', {
auto_register_worker: true,
worker_scope: '/',
debug: true,
});
// Create UCAN origin token
const tokenResponse = await plugin.newOriginToken({
audience_did: 'did:sonr:audience123',
attenuations: [{ can: ['sign', 'verify'], with: 'vault://my-vault' }],
});
```
</Tab>
<Tab title="Node.js">
```typescript
import { createMotorPluginForNode } from '@sonr.io/es/client/motor';
const plugin = await createMotorPluginForNode('http://localhost:8080', {
max_retries: 3,
retry_delay: 1000,
timeout: 5000,
debug: true,
});
```
</Tab>
</Tabs>
### Record Operations
<CodeGroup>
```typescript DWN Create
const createResult = await plugin.createRecord({
schema: 'https://schema.org/Person',
data: JSON.stringify({ name: 'Alice Smith' }),
is_encrypted: false,
});
```
```typescript DWN Read
const record = await plugin.readRecord({
record_id: createResult.record_id,
});
```
```typescript DWN Update
await plugin.updateRecord({
record_id: createResult.record_id,
data: JSON.stringify({ name: 'Alice Johnson' }),
});
```
```typescript DWN Delete
const deleteResult = await plugin.deleteRecord({
record_id: createResult.record_id,
});
```
</CodeGroup>
### Encrypted Records
```typescript
const sensitiveData = {
ssn: '123-45-6789',
medical_record: 'Confidential information',
};
const encryptedRecord = await plugin.createRecord({
schema: 'https://schema.org/MedicalRecord',
data: JSON.stringify(sensitiveData),
is_encrypted: true, // Enable encryption
});
// Automatic decryption on read
const decryptedRecord = await plugin.readRecord({
record_id: encryptedRecord.record_id,
});
```
## Deployment
<Tabs>
<Tab title="Development">
```bash
cd dist/wasm
python3 -m http.server 8080
# Access at http://localhost:8080/test.html
```
</Tab>
<Tab title="Production (Nginx)">
```nginx
location /motor/ {
alias /path/to/dist/wasm/;
# CORS and MIME types
add_header 'Access-Control-Allow-Origin' '*';
location ~ \.wasm$ {
add_header 'Content-Type' 'application/wasm';
}
}
```
</Tab>
<Tab title="CDN">
```html
<script src="https://cdn.example.com/motor/wasm_exec.js"></script>
<script>
navigator.serviceWorker.register(
'https://cdn.example.com/motor/motr-sw.js'
);
</script>
```
</Tab>
</Tabs>
## Troubleshooting
<Accordion title="Common Issues">
<Accordion.Panel title="Service Worker Not Registering">
- Ensure HTTPS or localhost
- Check browser console
- Verify service worker file path
</Accordion.Panel>
<Accordion.Panel title="WASM Module Loading">
- Check MIME type: `application/wasm`
- Verify CORS headers
- Match `wasm_exec.js` with Go version
</Accordion.Panel>
</Accordion>
## Browser Compatibility
| Browser | Minimum Version | Support |
|---------|----------------|---------|
| Chrome | 89+ | Full |
| Firefox | 89+ | Full |
| Safari | 15.4+ | Good |
| Edge | 89+ | Full |
<Warning>
Requires HTTPS or localhost for service worker functionality
</Warning>
## Performance Tips
<Tip>
- Use TinyGo for smaller WASM binaries
- Enable browser caching
- Lazy load Motor plugin
- Limit service worker scope
</Tip>
## Support
- **GitHub**: [Issues](https://github.com/sonr-io/sonr/issues)
- **Docs**: [Motor WASM Documentation](https://docs.sonr.io/motor-wasm)
- **Discord**: [Sonr Community](https://discord.gg/sonr)
-172
View File
@@ -1,172 +0,0 @@
---
title: "PDK Environment Configuration"
description: "Comprehensive guide to configuring the Pluggable Development Kit (PDK) for Sonr plugins"
icon: "gear"
---
# PDK Environment Configuration
The Pluggable Development Kit (PDK) provides a flexible configuration system for managing plugin environments, MPC enclaves, and runtime settings.
## Environment Variables
### Core PDK Variables
| Variable | Type | Description | Default |
| -------------- | -------- | --------------------------------- | ------------------ |
| `chain_id` | `string` | Target blockchain network | `"sonr-testnet-1"` |
| `enclave` | `object` | MPC enclave configuration | `{}` |
| `vault_config` | `object` | Vault and key management settings | `{}` |
| `log_level` | `string` | Logging verbosity | `"info"` |
## Enclave Configuration
### Basic Enclave Setup
```json
{
"enclave": {
"id": "unique-enclave-identifier",
"key_type": "secp256k1",
"threshold": 2,
"participants": [
{ "id": "participant1", "public_key": "..." },
{ "id": "participant2", "public_key": "..." }
]
}
}
```
### Advanced Enclave Parameters
```json
{
"enclave": {
"security_level": "high",
"attestation_mode": "remote",
"key_rotation_interval": "1h",
"backup_strategy": "distributed"
}
}
```
## Vault Configuration
### Key Management Settings
```json
{
"vault_config": {
"storage_backend": "ipfs",
"encryption": {
"algorithm": "aes-256-gcm",
"key_derivation": "pbkdf2"
},
"access_control": {
"mode": "role-based",
"default_role": "viewer"
}
}
}
```
## Logging Configuration
```json
{
"log_level": "debug",
"log_format": "json",
"log_outputs": [
{ "type": "stdout" },
{ "type": "file", "path": "/var/log/sonr/pdk.log" }
]
}
```
## Performance Tuning
```json
{
"performance": {
"max_concurrent_tasks": 10,
"task_timeout": "5m",
"memory_limit": "512MB",
"cpu_allocation": 2
}
}
```
## Security Hardening
```json
{
"security": {
"require_mfa": true,
"allowed_key_types": ["secp256k1", "ed25519"],
"audit_logging": true,
"rate_limiting": {
"max_requests_per_minute": 100
}
}
}
```
## Plugin-Specific Configuration
```json
{
"plugins": {
"motor": {
"mpc_mode": "distributed",
"token_generation_rate_limit": 10
},
"did": {
"supported_methods": ["did:key", "did:sonr"]
}
}
}
```
## Environment Loading Precedence
1. Environment Variables
2. Configuration Files
3. Default Values
## Best Practices
- Use environment-specific configurations
- Implement strict access controls
- Rotate encryption keys regularly
- Monitor and log configuration changes
- Use minimal privilege principles
## Example Configuration Loading
```go
func loadPDKConfiguration() (*PDKConfig, error) {
// Load from environment variables
config := &PDKConfig{}
// Override with config file if exists
configFile, err := ioutil.ReadFile("/etc/sonr/pdk.json")
if err == nil {
json.Unmarshal(configFile, config)
}
return config, nil
}
```
## Troubleshooting
- Check `log_level` for detailed diagnostics
- Validate JSON configuration syntax
- Verify key and enclave configurations
## Advanced Topics
For more complex PDK configurations, refer to:
- [DWN Plugin Documentation](/blockchain/modules/dwn/plugin)
- [DWN Architecture Overview](/blockchain/modules/dwn/architecture)
-438
View File
@@ -1,438 +0,0 @@
---
title: "Sign in with Sonr: Developer Guide"
description: "OAuth 2.0 authentication for decentralized applications with UCAN capabilities"
sidebarTitle: "Sign in with Sonr"
icon: "key"
---
<Info>
This guide covers OAuth 2.0 authentication for decentralized applications using Sonr's advanced Web3 capabilities.
</Info>
## Overview
Sign in with Sonr provides OAuth 2.0 authentication for decentralized applications, combining traditional OAuth flows with Web3 capabilities through UCAN (User Controlled Authorization Networks) delegation.
<CardGroup cols={2}>
<Card title="OAuth 2.0 + OIDC" icon="lock">
Industry-standard authentication with OpenID Connect
</Card>
<Card title="WebAuthn Support" icon="fingerprint">
Passwordless authentication with hardware security
</Card>
<Card title="UCAN Capabilities" icon="network">
Fine-grained permission delegation for Web3
</Card>
<Card title="Decentralized Identity" icon="id-card">
W3C DID-based identity management
</Card>
</CardGroup>
## Quick Start
### Installation
<CodeGroup>
```bash npm
npm install @sonr.io/ui
```
```bash pnpm
pnpm add @sonr.io/ui
```
```bash yarn
yarn add @sonr.io/ui
```
</CodeGroup>
### Basic Implementation
<CodeGroup>
```tsx React
import { SignInWithSonr } from '@sonr.io/ui';
function App() {
return (
<SignInWithSonr
clientId="your-client-id"
redirectUri="http://localhost:3000/callback"
scopes={['openid', 'profile', 'vault:read']}
/>
);
}
```
</CodeGroup>
## Configuration
### OAuth Client Registration
<CodeGroup>
```typescript Configuration
const clientConfig = {
clientId: 'your-client-id',
clientSecret: 'your-client-secret', // Only for confidential clients
redirectUris: ['http://localhost:3000/callback'],
grantTypes: ['authorization_code', 'refresh_token'],
responseTypes: ['code'],
scopes: ['openid', 'profile', 'vault:read', 'vault:write'],
tokenEndpointAuthMethod: 'none', // For public clients
};
```
</CodeGroup>
### Environment Variables
<CodeGroup>
```env OAuth Endpoints
# OAuth Endpoints
NEXT_PUBLIC_SONR_CLIENT_ID=your-client-id
NEXT_PUBLIC_REDIRECT_URI=http://localhost:3000/callback
NEXT_PUBLIC_AUTH_URL=https://auth.sonr.io/oauth/authorize
NEXT_PUBLIC_TOKEN_URL=https://auth.sonr.io/oauth/token
NEXT_PUBLIC_USERINFO_URL=https://auth.sonr.io/oauth/userinfo
```
</CodeGroup>
## OAuth Scopes & UCAN Capabilities
<Tabs>
<Tab title="Standard Scopes">
<table>
<thead>
<tr>
<th>Scope</th>
<th>Description</th>
<th>UCAN Capabilities</th>
</tr>
</thead>
<tbody>
<tr>
<td>`openid`</td>
<td>OpenID Connect identity</td>
<td>Basic identity claims</td>
</tr>
<tr>
<td>`profile`</td>
<td>User profile information</td>
<td>Name, picture, metadata</td>
</tr>
<tr>
<td>`email`</td>
<td>Email address</td>
<td>Email and verification status</td>
</tr>
<tr>
<td>`offline_access`</td>
<td>Refresh token issuance</td>
<td>Long-lived access</td>
</tr>
</tbody>
</table>
</Tab>
<Tab title="Vault Scopes">
<table>
<thead>
<tr>
<th>Scope</th>
<th>Description</th>
<th>UCAN Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>`vault:read`</td>
<td>Read vault contents</td>
<td>`vault/read`, `vault/list`</td>
</tr>
<tr>
<td>`vault:write`</td>
<td>Modify vault contents</td>
<td>`vault/write`, `vault/create`, `vault/update`</td>
</tr>
<tr>
<td>`vault:delete`</td>
<td>Delete vault items</td>
<td>`vault/delete`</td>
</tr>
<tr>
<td>`vault:admin`</td>
<td>Full vault control</td>
<td>All vault actions</td>
</tr>
</tbody>
</table>
</Tab>
</Tabs>
## Integration Examples
<Tabs>
<Tab title="React Hooks">
<CodeGroup>
```tsx React Hooks
import { useSignInWithSonr } from '@sonr.io/ui';
function LoginComponent() {
const {
user,
token,
isAuthenticated,
isLoading,
signIn,
signOut,
refreshToken,
} = useSignInWithSonr({
clientId: 'your-client-id',
redirectUri: 'http://localhost:3000/callback',
scopes: ['openid', 'profile', 'vault:read'],
});
if (isLoading) return <div>Loading...</div>;
if (isAuthenticated) {
return (
<div>
<p>Welcome, {user.name}!</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}
return <button onClick={signIn}>Sign in with Sonr</button>;
}
```
</CodeGroup>
</Tab>
<Tab title="Next.js App Router">
<CodeGroup>
```tsx Next.js Layout
// app/layout.tsx
import { AuthProvider } from '@/components/AuthProvider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}
```
```tsx Auth Provider
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { OAuth2Client } from '@sonr.io/ui';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [client] = useState(() => new OAuth2Client({
clientId: process.env.NEXT_PUBLIC_SONR_CLIENT_ID,
redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI,
}));
// ... authentication logic
return (
<AuthContext.Provider value={{ client, /* ... */ }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
```
</CodeGroup>
</Tab>
</Tabs>
## Advanced Features
### Custom Authorization
<CodeGroup>
```tsx Custom Authorization
<SignInWithSonr
clientId="your-client-id"
redirectUri="http://localhost:3000/callback"
authorizationUrl="https://auth.sonr.io/oauth/authorize"
state={generateRandomState()} // CSRF protection
scopes={['openid', 'profile', 'vault:admin']}
// Additional parameters
onAuthStart={() => console.log('Starting auth...')}
onAuthError={(error) => console.error('Auth failed:', error)}
/>
```
</CodeGroup>
### Token Management
<CodeGroup>
```typescript Token Management
const client = new OAuth2Client(config);
// Check authentication status
if (client.isAuthenticated()) {
// Get current access token
const accessToken = client.getAccessToken();
// Refresh token before expiry
const newToken = await client.refreshToken();
// Get user information
const userInfo = await client.getUserInfo();
// Revoke tokens on logout
await client.logout();
}
```
</CodeGroup>
## Security Considerations
<Warning>
Always implement robust security practices when integrating authentication.
</Warning>
### PKCE Implementation
<CodeGroup>
```typescript PKCE Configuration
const client = new OAuth2Client({
clientId: 'public-client',
pkce: true, // Enabled by default for public clients
});
```
</CodeGroup>
### State Parameter Prevention
<CodeGroup>
```typescript State Validation
// Generate random state
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
// Validate on callback
const returnedState = params.get('state');
const savedState = sessionStorage.getItem('oauth_state');
if (returnedState !== savedState) {
throw new Error('State mismatch - possible CSRF attack');
}
```
</CodeGroup>
## Troubleshooting
<Accordion title="Common Authentication Issues">
<AccordionItem title="CORS Errors">
- Ensure your redirect URI is whitelisted
- Check allowed origins in OAuth server config
</AccordionItem>
<AccordionItem title="Invalid Grant">
- Authorization code can only be used once
- Code expires after 10 minutes
- Verify redirect URI matches exactly
</AccordionItem>
<AccordionItem title="Token Expiry">
- Implement automatic refresh before expiry
- Handle refresh token rotation
</AccordionItem>
</Accordion>
<Note>
Enable debug mode for additional troubleshooting insights:
```typescript
const client = new OAuth2Client({
clientId: 'your-client-id',
debug: true, // Enable console logging
});
```
</Note>
## API Reference
### SignInWithSonr Props
```typescript
interface SignInWithSonrProps {
clientId: string;
redirectUri: string;
authorizationUrl?: string;
scopes?: string[];
state?: string;
variant?: 'default' | 'outline' | 'ghost' | 'dark';
size?: 'default' | 'sm' | 'lg';
isLoading?: boolean;
text?: string;
showLogo?: boolean;
onAuthStart?: () => void;
onAuthError?: (error: Error) => void;
}
```
## Resources
<CardGroup cols={2}>
<Card
title="OAuth 2.0 Specification"
href="https://datatracker.ietf.org/doc/html/rfc6749"
>
RFC 6749 - OAuth 2.0 Authorization Framework
</Card>
<Card
title="OpenID Connect"
href="https://openid.net/specs/openid-connect-core-1_0.html"
>
Core specification for identity layers
</Card>
<Card
title="UCAN Specification"
href="https://github.com/ucan-wg/spec"
>
User Controlled Authorization Networks
</Card>
<Card
title="WebAuthn"
href="https://www.w3.org/TR/webauthn/"
>
Web Authentication API specification
</Card>
</CardGroup>
## Support
<CardGroup cols={3}>
<Card
title="GitHub"
icon="github"
href="https://github.com/sonr-io/sonr"
>
Report issues or contribute
</Card>
<Card
title="Documentation"
icon="book"
href="https://sonr.dev"
>
Explore full documentation
</Card>
<Card
title="Discord"
icon="discord"
href="https://discord.gg/sonr"
>
Join our community
</Card>
</CardGroup>
-286
View File
@@ -1,286 +0,0 @@
---
title: "Sonr Vault Plugin with Dexie.js Persistence"
description: "Comprehensive guide to using the Sonr Vault Plugin with persistent storage and multi-account support"
sidebarTitle: "Vault Plugin Usage"
icon: "lock"
---
# Sonr Vault Plugin: Persistent Storage and Account Management
## Overview
The Sonr Vault Plugin provides a powerful, secure, and flexible way to manage cryptographic operations with persistent storage using Dexie.js and IndexedDB. This guide will walk you through the plugin's features, setup, and advanced usage patterns.
<Callout type="info">
**Key Features**
- 🔐 Account-based database separation
- 💾 Automatic token persistence
- 🔄 Cross-browser IndexedDB support
- ⚡ Backward compatibility
- 🧹 Automatic token and session cleanup
</Callout>
## Installation
Install the Sonr Vault Plugin in your project:
<CodeGroup>
```bash npm
npm install @sonr.io/es
```
```bash yarn
yarn add @sonr.io/es
```
```bash pnpm
pnpm add @sonr.io/es
```
</CodeGroup>
## Basic Usage
### Without Persistence (Default)
The vault plugin is designed to be backward compatible. By default, it operates without persistent storage:
```typescript
import { createVaultClient } from '@sonr.io/es/plugins/vault';
// Create a vault client without persistence
const vault = createVaultClient();
// Initialize the vault
await vault.initialize();
// Create tokens and perform operations as before
const token = await vault.newOriginToken({
audience_did: 'did:example:123',
});
```
### With Persistence Enabled
Enable persistent storage with a simple configuration:
```typescript
import { createVaultClient } from '@sonr.io/es/plugins/vault';
// Create a vault client with persistence
const vault = createVaultClient({
enablePersistence: true,
autoCleanup: true, // Automatically clean up expired tokens
cleanupInterval: 3600000 // Cleanup every hour (in milliseconds)
});
// Initialize with an account address for database separation
const accountAddress = 'sonr1abc123...';
await vault.initialize('/plugin.wasm', accountAddress);
// Tokens are now automatically persisted
const token = await vault.newOriginToken({
audience_did: 'did:example:123',
});
```
## Advanced Features
### Multi-Account Support
Seamlessly switch between accounts and manage their individual databases:
```typescript
// Switch to a different account
await vault.switchAccount('sonr1account2');
// List all accounts with persisted data
const accounts = await vault.listPersistedAccounts();
// Remove an account's data
await vault.removeAccount('sonr1account1');
```
### Token Management
Manually manage persisted tokens:
```typescript
// Get all saved tokens
const tokens = await vault.getPersistedTokens();
// Save a specific token
await vault.saveToken({
token: 'eyJ...',
issuer: 'did:sonr:example',
address: 'sonr1abc...',
});
// Remove expired tokens
await vault.removeExpiredTokens();
```
### State Management
Control vault state persistence:
```typescript
// Manually save current state
await vault.persistState();
// Load persisted state
const state = await vault.loadPersistedState();
// Clear all persisted data for the current account
await vault.clearPersistedState();
```
## Storage Management
Use the `VaultStorageManager` for advanced storage operations:
```typescript
import { VaultStorageManager } from '@sonr.io/es/plugins/vault';
const storageManager = new VaultStorageManager({
enablePersistence: true,
});
// Request persistent storage
const isPersisted = await storageManager.requestPersistentStorage();
// Check storage status and estimate
const status = await storageManager.tryPersistWithoutPromptingUser();
const estimate = await storageManager.getStorageEstimate();
```
## Configuration Options
Customize the vault's storage behavior:
<TypeTable
columns={[
{ name: 'enablePersistence', type: 'boolean', description: 'Enable IndexedDB storage' },
{ name: 'storageQuotaRequest', type: 'number', description: 'Storage quota to request in bytes' },
{ name: 'autoCleanup', type: 'boolean', description: 'Enable automatic token/session cleanup' },
{ name: 'cleanupInterval', type: 'number', description: 'Cleanup interval in milliseconds' }
]}
/>
## Browser Compatibility
The Vault Plugin works with most modern browsers:
<Table>
<TableHead>
<TableRow>
<TableCell>Browser</TableCell>
<TableCell>Minimum Version</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Chrome/Edge</TableCell>
<TableCell>23+</TableCell>
</TableRow>
<TableRow>
<TableCell>Firefox</TableCell>
<TableCell>16+</TableCell>
</TableRow>
<TableRow>
<TableCell>Safari</TableCell>
<TableCell>10+</TableCell>
</TableRow>
<TableRow>
<TableCell>Opera</TableCell>
<TableCell>15+</TableCell>
</TableRow>
<TableRow>
<TableCell>iOS Safari</TableCell>
<TableCell>10+</TableCell>
</TableRow>
<TableRow>
<TableCell>Chrome for Android</TableCell>
<TableCell>All versions</TableCell>
</TableRow>
</TableBody>
</Table>
## Storage Limits
Storage availability varies by browser:
<Table>
<TableHead>
<TableRow>
<TableCell>Browser</TableCell>
<TableCell>Storage Limit</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Chrome/Edge</TableCell>
<TableCell>60% of total disk space</TableCell>
</TableRow>
<TableRow>
<TableCell>Firefox</TableCell>
<TableCell>50% of free disk space</TableCell>
</TableRow>
<TableRow>
<TableCell>Safari</TableCell>
<TableCell>Starts at 1GB, can request more</TableCell>
</TableRow>
<TableRow>
<TableCell>Mobile Browsers</TableCell>
<TableCell>Varies by device</TableCell>
</TableRow>
</TableBody>
</Table>
## Security Considerations
<Callout type="warning">
- Databases are isolated by account address
- No private keys or sensitive cryptographic material are stored
- Only UCAN tokens and metadata are persisted
- Always use HTTPS in production
- Consider encrypting sensitive data before storage
</Callout>
## Troubleshooting
### Storage Not Persisting
1. Verify you're running on HTTPS
2. Check that IndexedDB is enabled in browser settings
3. Confirm available storage quota
4. Explicitly request persistent storage
```typescript
try {
await vault.initialize('/plugin.wasm', accountAddress);
} catch (error) {
if (error.code === 'VAULT_NOT_INITIALIZED') {
// Handle initialization error
}
}
```
## Migration Guide
To migrate from non-persistent to persistent storage:
```typescript
// Before (non-persistent)
const vault = createVaultClient();
await vault.initialize();
// After (with persistence)
const vault = createVaultClient({
enablePersistence: true,
});
await vault.initialize('/plugin.wasm', accountAddress);
```
<Callout type="success">
**No other code changes are required!** All existing methods work the same way.
</Callout>
-191
View File
@@ -1,191 +0,0 @@
---
title: Getting Started with Browser/ESM
description: A quick start guide for using Sonr in web browsers with WebAuthn integration
sidebarTitle: Browser Quickstart
icon: "globe"
---
This guide will walk you through creating a simple web application that interacts with the Sonr network directly from the browser. We will use the Sonr JavaScript SDK to create a new user identity, claim a Vault, and send a transaction.
## Prerequisites
- A local Sonr network running. See the [Validator Setup Guide](/quickstart/validators) for instructions
- A modern web browser with WebAuthn support (Chrome, Firefox, Safari, Edge)
## 1. Project Setup
<Steps>
<Step>
### Create an HTML File
Create a new `index.html` file and add the following basic structure:
```html
<!DOCTYPE html>
<html>
<head>
<title>Sonr Browser Quickstart</title>
<script type="module" src="app.js"></script>
</head>
<body>
<h1>Sonr Browser Quickstart</h1>
<button id="create-identity">Create Identity</button>
<div id="output"></div>
</body>
</html>
```
</Step>
<Step>
### Create a JavaScript File
Create a new `app.js` file in the same directory. This is where we will write our application logic.
</Step>
<Step>
### Install the Sonr SDK
For this quickstart, we will use the Sonr SDK from a CDN. Add the following script tag to the `<head>` of your `index.html` file:
```html
<script type="module">
import { Sonr, WebAuthn } from "https://cdn.jsdelivr.net/npm/@sonr/sdk";
window.Sonr = Sonr;
window.WebAuthn = WebAuthn;
</script>
```
</Step>
</Steps>
## 2. Creating an Identity
Now, let's add the logic to create a new user identity and claim a Vault.
<Steps>
<Step>
### Add Event Listener
In `app.js`, add an event listener to the "Create Identity" button:
```javascript
document
.getElementById("create-identity")
.addEventListener("click", async () => {
const output = document.getElementById("output");
output.innerHTML = "Creating identity...";
try {
// Code to create identity will go here
} catch (error) {
output.innerHTML = `Error: ${error.message}`;
}
});
```
</Step>
<Step>
### Implement Identity Creation
Inside the event listener, use the `WebAuthn.createCredential` method to create a new WebAuthn credential and the `Sonr.claimVault` method to claim a new Vault on the network.
```javascript
// Inside the try block
const credential = await WebAuthn.createCredential({
rp: { name: "Sonr Quickstart" },
user: {
id: new Uint8Array(16), // Should be a unique user ID
name: "user@example.com",
displayName: "Test User",
},
});
output.innerHTML = `Credential created: ${credential.id}`;
const sonr = new Sonr({ httpUrl: "http://localhost:1317" });
const vault = await sonr.claimVault(credential);
output.innerHTML = `Vault claimed! DID: ${vault.did}`;
```
<Note type="warning">
In a real application, the user ID should be a unique and stable identifier
for the user, not a random value.
</Note>
</Step>
</Steps>
## 3. Running the Application
To run the application, you need a simple web server. You can use the `http-server` package for this.
```bash
# Install http-server
npm install -g http-server
# Start the server
http-server
```
Now, open your browser and navigate to `http://localhost:8080`. When you click the "Create Identity" button, your browser will prompt you to create a new passkey using your device's biometrics or a security key.
## 4. Sending a Transaction
Once you have a Vault, you can use it to send transactions.
<Steps>
<Step>
### Add a Send Button
Add a new button to your `index.html` file:
```html
<button id="send-transaction" disabled>Send Transaction</button>
```
</Step>
<Step>
### Implement Transaction Sending
In `app.js`, add an event listener to the new button. This will use the `sonr.send` method to send a transaction.
```javascript
let vaultInstance;
// After claiming the vault...
vaultInstance = vault;
document.getElementById("send-transaction").disabled = false;
document
.getElementById("send-transaction")
.addEventListener("click", async () => {
const output = document.getElementById("output");
output.innerHTML = "Sending transaction...";
try {
const result = await vaultInstance.send({
to: "snr1..._recipient_address_...",
amount: "1000000usnr", // 1 SNR
});
output.innerHTML = `Transaction successful! TxHash: ${result.txhash}`;
} catch (error) {
output.innerHTML = `Error: ${error.message}`;
}
});
```
</Step>
</Steps>
## Next Steps
Congratulations! You have successfully created a web application that interacts with the Sonr network. From here, you can explore more advanced features:
- **Service Registration**: Register your application as a trusted service on the network
- **UCAN Authorization**: Request and manage user permissions for your application
- **Cross-Chain Operations**: Interact with other blockchains through IBC
-521
View File
@@ -1,521 +0,0 @@
---
title: Getting Started with Golang
description: A quick start guide for building applications with Sonr using the Go Client SDK
sidebarTitle: Golang Quickstart
icon: "golang"
---
This guide provides a walkthrough for setting up a Go project to interact with the Sonr network using the official Go Client SDK. You will learn how to configure the client, manage keys, query the blockchain, send transactions, and use advanced features like WebAuthn gasless transactions.
## Prerequisites
- A local Sonr network running. See the [Validator Setup Guide](/quickstart/validators) for instructions
- Go version 1.24 or higher
## 1. Project Setup
<Steps>
<Step>
### Initialize a Go Module
Create a new directory for your project and initialize a Go module:
```bash
mkdir sonr-go-quickstart
cd sonr-go-quickstart
go mod init github.com/your-username/sonr-go-quickstart
```
</Step>
<Step>
### Add the Sonr Client SDK Dependency
Add the Sonr Client SDK to your project's dependencies:
```bash
go get github.com/sonr-io/sonr/client
```
</Step>
</Steps>
## 2. Client Configuration and Setup
Let's set up the Sonr client with proper configuration and key management.
<Steps>
<Step>
### Create the Main File
Create a new file named `main.go`.
</Step>
<Step>
### Initialize the Client
Add the following code to `main.go` to initialize the Sonr client:
```go
package main
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/keys"
"github.com/sonr-io/sonr/client/sonr"
"github.com/sonr-io/sonr/client/tx"
)
func main() {
// Use local network configuration
cfg := config.LocalNetwork()
// Establish gRPC connection
conn, err := grpc.Dial(
cfg.GRPC,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal("Failed to connect:", err)
}
defer conn.Close()
// Create the Sonr client
client, err := sonr.NewClient(&cfg, conn)
if err != nil {
log.Fatal("Failed to create client:", err)
}
fmt.Println("Sonr client initialized successfully!")
fmt.Printf("Connected to: %s\n", cfg.ChainID)
}
```
</Step>
<Step>
### Create a Keyring Manager
Add key management to handle wallet operations:
```go
// Initialize keyring manager (using test backend for development)
keyringManager, err := keys.NewKeyringManager(
"test", // backend: test, file, os
".sonr-keys", // directory for keys
cfg.ChainID, // chain ID
)
if err != nil {
log.Fatal("Failed to create keyring:", err)
}
// Create a new wallet
walletIdentity, mnemonic, err := keyringManager.CreateWallet(
context.Background(),
"my-wallet", // wallet name
"", // passphrase (empty for test backend)
)
if err != nil {
log.Fatal("Failed to create wallet:", err)
}
fmt.Printf("Wallet created!\n")
fmt.Printf("Address: %s\n", walletIdentity.Address)
fmt.Printf("DID: %s\n", walletIdentity.DID)
fmt.Printf("Mnemonic: %s\n", mnemonic)
```
<Note type="warning">
**Important**: In production, use secure keyring backends like "os" or "file" with proper passphrase protection. Never expose mnemonics in your code.
</Note>
</Step>
</Steps>
## 3. Querying the Blockchain
Now, let's query the blockchain using the unified query client.
<Steps>
<Step>
### Create a Query Client
Add the query client to your application:
```go
import (
"github.com/sonr-io/sonr/client/query"
)
// Create query client
queryClient, err := query.NewQueryClient(conn, &cfg)
if err != nil {
log.Fatal("Failed to create query client:", err)
}
```
</Step>
<Step>
### Query Account Balance
Query an account's balance:
```go
// Query account balance
address := walletIdentity.Address // or any other address
balance, err := queryClient.Balance(
context.Background(),
address,
cfg.StakingDenom, // "usnr"
)
if err != nil {
log.Printf("Failed to query balance: %v", err)
} else {
fmt.Printf("Balance for %s: %s %s\n",
address,
balance.Balance.Amount.String(),
balance.Balance.Denom,
)
}
// Query all balances for an account
allBalances, err := queryClient.AllBalances(
context.Background(),
address,
nil, // pagination
)
if err != nil {
log.Printf("Failed to query all balances: %v", err)
} else {
fmt.Printf("All balances: %v\n", allBalances.Balances)
}
```
</Step>
<Step>
### Query Module-Specific Data
Query DID documents and other module data:
```go
import (
"github.com/sonr-io/sonr/client/modules/did"
)
// Create DID module client
didClient := did.NewDIDClient()
// Query DID document (if exists)
didID := "did:sonr:example123"
didDoc, err := queryClient.GetDID(context.Background(), didID)
if err != nil {
log.Printf("DID not found: %v", err)
} else {
fmt.Printf("DID Document: %+v\n", didDoc)
}
// List all DIDs with pagination
didList, err := queryClient.ListDIDs(
context.Background(),
&query.ListOptions{
Limit: 10,
Offset: 0,
},
)
if err != nil {
log.Printf("Failed to list DIDs: %v", err)
} else {
fmt.Printf("Found %d DIDs\n", len(didList.DIDs))
}
```
</Step>
</Steps>
## 4. Building and Broadcasting Transactions
Let's build and broadcast transactions using the transaction builder.
<Steps>
<Step>
### Create a Transaction Builder
Initialize the transaction builder with gas estimation:
```go
// Create transaction builder
txBuilder, err := tx.NewTxBuilder(&cfg, conn)
if err != nil {
log.Fatal("Failed to create tx builder:", err)
}
// Create broadcaster
broadcaster, err := tx.NewBroadcaster(&cfg, conn)
if err != nil {
log.Fatal("Failed to create broadcaster:", err)
}
// Create gas estimator
gasEstimator := tx.NewGasEstimator(conn, &cfg)
```
</Step>
<Step>
### Send Tokens Between Accounts
Build and broadcast a bank send transaction:
```go
import (
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)
// Create a second wallet to receive funds
receiver, _, err := keyringManager.CreateWallet(
context.Background(),
"receiver-wallet",
"",
)
if err != nil {
log.Fatal("Failed to create receiver wallet:", err)
}
// Create send message
amount := sdk.NewCoins(sdk.NewInt64Coin("usnr", 1000000)) // 1 SNR
sendMsg := &banktypes.MsgSend{
FromAddress: walletIdentity.Address,
ToAddress: receiver.Address,
Amount: amount,
}
// Build transaction
txBuilder = txBuilder.
AddMessage(sendMsg).
WithMemo("Test transaction").
WithGasLimit(200000)
// Estimate gas
gasEstimate, err := gasEstimator.EstimateGas(
context.Background(),
[]sdk.Msg{sendMsg},
)
if err != nil {
log.Printf("Gas estimation failed: %v", err)
} else {
fmt.Printf("Estimated gas: %d\n", gasEstimate.GasLimit)
txBuilder = txBuilder.WithGasLimit(gasEstimate.GasLimit)
}
// Calculate and set fee
fee := gasEstimator.CalculateFee(
gasEstimate.GasLimit,
cfg.GasPrice,
cfg.StakingDenom,
)
txBuilder = txBuilder.WithFee(fee)
// Sign the transaction
signedTx, err := txBuilder.Sign(context.Background(), keyringManager)
if err != nil {
log.Fatal("Failed to sign transaction:", err)
}
// Broadcast the transaction
result, err := broadcaster.BroadcastTx(context.Background(), signedTx)
if err != nil {
log.Fatal("Failed to broadcast transaction:", err)
}
fmt.Printf("Transaction successful!\n")
fmt.Printf("TxHash: %s\n", result.TxHash)
fmt.Printf("Height: %d\n", result.Height)
fmt.Printf("Gas Used: %d\n", result.GasUsed)
```
</Step>
</Steps>
## 5. WebAuthn Gasless Transactions (Advanced)
Sonr supports gasless WebAuthn registration, allowing users to onboard without holding tokens.
<Steps>
<Step>
### Initialize WebAuthn Client
Set up the WebAuthn client for gasless operations:
```go
import (
"github.com/sonr-io/sonr/client/auth"
)
// Create WebAuthn client
webauthnClient := auth.NewWebAuthnClient(
keyringManager,
"localhost", // Relying Party ID
"Sonr Local", // Relying Party Name
)
// Create gasless transaction manager
gaslessManager := auth.NewGaslessTransactionManager(
txBuilder,
broadcaster,
&cfg,
)
// Create WebAuthn gasless client
gaslessClient := auth.NewWebAuthnGaslessClient(
webauthnClient,
gaslessManager,
&cfg,
)
```
</Step>
<Step>
### Initiate Gasless Registration
Start a gasless WebAuthn registration:
```go
// Begin gasless registration
registrationResult, err := gaslessClient.RegisterGasless(
context.Background(),
"alice", // username
"Alice Smith", // display name
)
if err != nil {
log.Fatal("Failed to initiate registration:", err)
}
fmt.Printf("Registration initiated!\n")
fmt.Printf("Gasless eligible: %v\n", registrationResult.GaslessEligible)
fmt.Printf("Estimated gas: %d\n", registrationResult.EstimatedGas)
// The challenge would be sent to a browser for completion
// In a real application, you'd handle the browser response
```
</Step>
<Step>
### Check Gasless Eligibility
Verify if a transaction is eligible for gasless processing:
```go
// Check if messages are eligible for gasless
msgs := []sdk.Msg{
&didtypes.MsgRegisterWebAuthnCredential{
Controller: "sonr1...",
Username: "alice",
},
}
isEligible := gaslessManager.IsEligibleForGasless(msgs)
fmt.Printf("Transaction eligible for gasless: %v\n", isEligible)
// Estimate gas for gasless transaction
gasNeeded := gaslessManager.EstimateGaslessGas(
"/did.v1.MsgRegisterWebAuthnCredential",
)
fmt.Printf("Gas needed for gasless WebAuthn: %d\n", gasNeeded)
```
</Step>
</Steps>
## 6. Working with DID Module
Create and manage decentralized identities:
<Steps>
<Step>
### Create a DID Document
```go
import (
didtypes "github.com/sonr-io/sonr/x/did/types"
)
// Create DID document
didDoc := &didtypes.DidDocument{
Id: "did:sonr:" + walletIdentity.Address,
Controller: walletIdentity.Address,
VerificationMethod: []*didtypes.VerificationMethod{
{
Id: "did:sonr:" + walletIdentity.Address + "#key-1",
VerificationMethodKind: didtypes.VerificationMethodKind_Ed25519VerificationKey2020,
Controller: "did:sonr:" + walletIdentity.Address,
PublicKeyMultibase: "z6MkhaXgBZD...", // Your public key
},
},
Service: []*didtypes.Service{
{
Id: "did:sonr:" + walletIdentity.Address + "#dwn",
ServiceKind: didtypes.ServiceKind_DecentralizedWebNode,
SingleEndpoint: "https://dwn.example.com",
},
},
}
// Create the message
createDIDMsg, err := didClient.CreateDID(
walletIdentity.Address,
didDoc,
)
if err != nil {
log.Fatal("Failed to create DID message:", err)
}
// Add to transaction and broadcast
txBuilder = txBuilder.
ClearMessages().
AddMessage(createDIDMsg).
WithMemo("Create DID")
// Sign and broadcast as shown earlier
```
</Step>
</Steps>
## Next Steps
This quickstart has covered the fundamentals of the Sonr Go Client SDK:
- **Client Configuration**: Setting up connections and network configurations
- **Key Management**: Creating and managing wallets with the keyring
- **Querying**: Reading blockchain state and module data
- **Transactions**: Building, signing, and broadcasting transactions
- **Gas Estimation**: Calculating optimal gas limits and fees
- **WebAuthn**: Gasless onboarding with WebAuthn credentials
- **DID Module**: Creating decentralized identities
### Advanced Topics to Explore:
- **DWN Module**: Manage decentralized web nodes and data records
- **Service Module**: Register and verify services with domain verification
- **UCAN Integration**: Implement capability-based authorization
- **Multi-signature**: Create and manage multi-sig accounts
- **IBC Transfers**: Cross-chain token transfers
- **Custom Modules**: Interact with your own custom modules
### Useful Resources:
- [Client SDK API Reference](https://pkg.go.dev/github.com/sonr-io/sonr/client)
- [Cosmos SDK Documentation](https://docs.cosmos.network)
- [Sonr GitHub Repository](https://github.com/sonr-io/sonr)
-190
View File
@@ -1,190 +0,0 @@
---
title: Getting Started with ReactJS
description: A quick start guide for building applications with Sonr using JavaScript and TypeScript
sidebarTitle: React Quickstart
icon: "react"
---
This guide will show you how to set up a Node.js project with TypeScript and use the Sonr SDK to interact with the Sonr network.
## Prerequisites
- A local Sonr network running. See the [Validator Setup Guide](/quickstart/validators) for instructions
- Node.js version 16 or higher
- npm or yarn
## 1. Project Setup
<Steps>
<Step>
### Initialize a New Project
Create a new directory for your project and initialize it with npm:
```bash
mkdir sonr-ts-quickstart
cd sonr-ts-quickstart
npm init -y
```
</Step>
<Step>
### Install Dependencies
Install the Sonr SDK and TypeScript:
```bash
npm install @sonr/sdk typescript ts-node @types/node
```
</Step>
<Step>
### Configure TypeScript
Create a `tsconfig.json` file in your project root with the following configuration:
```json
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
```
</Step>
</Steps>
## 2. Creating a Wallet
Now, let's create a new TypeScript file and add the logic to create a new Sonr wallet.
<Steps>
<Step>
### Create the Main File
Create a new file named `index.ts`.
</Step>
<Step>
### Implement Wallet Creation
Add the following code to `index.ts` to create a new wallet and log its address and mnemonic:
```typescript
import { Sonr } from "@sonr/sdk";
async function main() {
console.log("Creating a new Sonr wallet...");
const sonr = new Sonr({ httpUrl: "http://localhost:1317" });
const wallet = await sonr.createWallet();
console.log(`Wallet created!`);
console.log(`Address: ${wallet.address}`);
console.log(`Mnemonic: ${wallet.mnemonic}`);
}
main().catch(console.error);
```
<Note type="warning">
**Important**: In a real application, you must store the mnemonic securely.
Never expose it in client-side code.
</Note>
</Step>
<Step>
### Run the Script
Execute the script using `ts-node`:
```bash
npx ts-node index.ts
```
You should see the new wallet's address and mnemonic printed to the console.
</Step>
</Steps>
## 3. Querying the Blockchain
Let's query the blockchain to get the balance of our new wallet.
<Steps>
<Step>
### Get Account Balance
Modify your `index.ts` file to query the account balance after creating the wallet. You will need to fund this account from the localnet faucet for it to have a balance.
```typescript
// ... after creating the wallet
console.log("Querying account balance...");
// The localnet validator has funds, so we'll use its address for the query
const validatorAddress = "snr1..._validator_address_..."; // Replace with the actual validator address from your localnet
const balance = await sonr.getAccountBalance(validatorAddress);
console.log(`Balance for ${validatorAddress}:`, balance);
```
</Step>
<Step>
### Run the Script Again
Run the script to see the account balance:
```bash
npx ts-node index.ts
```
</Step>
</Steps>
## 4. Sending a Transaction
Finally, let's send a transaction from one account to another.
<Steps>
<Step>
### Implement Transaction Sending
For this step, you will need two wallets. You can create a second one using the same `createWallet` method. Ensure both wallets have funds from the localnet faucet.
```typescript
// ... inside your main function
const wallet1 = await sonr.createWallet(); // Fund this from the faucet
const wallet2 = await sonr.createWallet();
console.log(`Sending 1 SNR from ${wallet1.address} to ${wallet2.address}`);
const result = await wallet1.send({
to: wallet2.address,
amount: "1000000usnr", // 1 SNR
});
console.log(`Transaction successful! TxHash: ${result.txhash}`);
```
</Step>
</Steps>
## Next Steps
This quickstart has shown you the basics of interacting with the Sonr network using our TypeScript SDK. You can now explore more advanced topics:
- **Service Registration**: Register your application as a trusted service
- **UCAN Authorization**: Implement capability-based permissions
- **Smart Contract Interaction**: Call and query smart contracts on the Sonr network
@@ -1,155 +0,0 @@
---
title: "DWN Plugin Architecture"
description: "Deep dive into the Decentralized Web Node (DWN) plugin system and integration"
sidebarTitle: "WASM Architecture"
icon: "puzzle"
---
# DWN Plugin Architecture
The Sonr project implements a flexible and secure plugin system for Decentralized Web Nodes (DWN), enabling modular and extensible functionality.
## Overview
The plugin architecture is designed to:
- Support dynamic loading of WebAssembly (WASM) plugins
- Provide a standardized interface for plugin interactions
- Enable secure, isolated execution of plugins
## Core Components
### Plugin Manager
The `PluginManager` manages plugin lifecycle and interactions:
```go
type PluginManager struct {
plugins map[string]Plugin
actors map[string]Actor
}
type Plugin interface {
Initialize(config map[string]any) error
Execute(method string, payload []byte) ([]byte, error)
Close() error
}
```
### Plugin Configuration
Plugins are configured through a structured configuration:
```go
type PluginConfig struct {
ID string // Unique plugin identifier
Type string // Plugin type (e.g., "motor", "crypto")
Path string // WASM module path
Environment map[string]any // Plugin-specific environment variables
}
```
## Loading and Initializing Plugins
### Basic Plugin Loading
```go
func (pm *PluginManager) LoadPlugin(config PluginConfig) error {
// Load WASM module
module, err := extism.Load(config.Path)
if err != nil {
return err
}
// Initialize plugin
plugin := &WASMPlugin{
module: module,
config: config,
}
// Store in plugin registry
pm.plugins[config.ID] = plugin
}
```
### Actor-Based Plugin Management
```go
func (pm *PluginManager) CreateActor(pluginID string) (*Actor, error) {
plugin, exists := pm.plugins[pluginID]
if !exists {
return nil, errors.New("plugin not found")
}
actor := NewActor(plugin)
pm.actors[actor.ID] = actor
return actor, nil
}
```
## Plugin Execution Workflow
1. Plugin is loaded from WASM module
2. Configuration is applied
3. Plugin is initialized
4. Specific methods can be invoked through a standardized interface
### Example Plugin Execution
```go
func ExecutePluginMethod(pluginID, method string, payload []byte) ([]byte, error) {
plugin := pluginManager.plugins[pluginID]
return plugin.Execute(method, payload)
}
```
## Configuration and Environment
### Plugin Environment Variables
```json
{
"motor_plugin": {
"enclave_config": { ... },
"chain_id": "sonr-testnet-1",
"log_level": "debug"
}
}
```
## Error Handling and Logging
```go
type PluginError struct {
Code string
Message string
Details map[string]any
}
```
## Security Considerations
- WASM plugins run in an isolated sandbox
- Limited access to system resources
- Runtime restrictions prevent malicious behavior
- Cryptographic verification of plugin modules
## Plugin Types
1. **Crypto Plugins**: Cryptographic operations
2. **Motor Plugins**: MPC and token management
3. **DID Plugins**: Decentralized Identity operations
4. **Custom Plugins**: Application-specific extensions
## Best Practices
- Keep plugins small and focused
- Use standardized interfaces
- Implement comprehensive error handling
- Validate all plugin inputs
- Monitor plugin performance
## Advanced Configuration
For advanced plugin configuration and deployment, refer to the [PDK Configuration Guide](/blockchain/modules/dwn/configuration).
View File
-173
View File
@@ -1,173 +0,0 @@
---
title: "@sonr.io/ui"
sidebarTitle: "@sonr.io/ui"
description: "A package for Sonr's centralized shadcn/ui component library"
icon: "palette"
---
# @sonr.io/ui
## Overview
The `@sonr.io/ui` package serves as our centralized shadcn/ui component library, providing a consistent and accessible design system across Sonr's ecosystem. Leveraging the power of shadcn/ui, we've created a fully customizable and type-safe component library with a primary color of `#17c2ff`.
<Callout type="info">
Our UI package is built to provide maximum flexibility while maintaining
strict design consistency.
</Callout>
## Component Migration Strategy
### Before: Custom Button Component
Previously, our Button component relied on manual variant classes and custom implementations:
```tsx
// Old Implementation
const Button = ({ variant, className, ...props }) => {
const variantClasses = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
// Multiple manual variant definitions
};
return (
<button className={`${variantClasses[variant]} ${className}`} {...props} />
);
};
```
### After: Shadcn Button Implementation
Our new implementation uses `cva` (class-variance-authority) and the `cn` utility for robust variant management:
```tsx
// New Implementation
import { buttonVariants } from "@sonr.io/ui/components/ui/button";
import { cn } from "@sonr.io/ui/lib/utils";
const Button = ({
variant = "default",
size = "default",
className,
...props
}) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
};
```
<Callout type="success">
Key Improvements: - Type-safe variant management - Enhanced accessibility -
Consistent theming - Reduced bundle size
</Callout>
## Theme Configuration
### Color System
Our primary theme color is `#17c2ff`, defined in HSL format for maximum flexibility:
```css
:root {
--primary-h: 200; /* Hue */
--primary-s: 100%; /* Saturation */
--primary-l: 59%; /* Lightness */
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
}
```
### CSS Variables Structure
```css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 200 100% 59%;
--primary-foreground: 210 40% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
/* Additional theme variables */
}
```
## Usage Guide
### Importing Components
Import components directly from the `@sonr.io/ui` package:
```tsx
import { Button } from "@sonr.io/ui/components/ui/button";
import { Input } from "@sonr.io/ui/components/ui/input";
```
### Adding New Components
Use the shadcn CLI within the `packages/ui` directory:
```bash
# Navigate to packages/ui
cd packages/ui
# Add a new component
npx shadcn-ui@latest add button
```
<Callout type="warning">
Always add components from the `packages/ui` directory to maintain our
centralized component management.
</Callout>
## Architecture Patterns
### Monorepo Structure
```
sonr/
├── packages/
│ └── ui/
│ ├── components/
│ │ └── ui/
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── ...
│ ├── lib/
│ │ └── utils.ts
│ └── styles/
│ └── globals.css
```
### Key Principles
- **Single Source of Truth**: All UI components live in `packages/ui`
- **No Local `components.json`**: Centralized configuration
- **Turbo-powered Build Pipeline**: Efficient component compilation
## Global Styles Integration
In your application's main entry point:
```tsx
import "@sonr.io/ui/styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
```
## Best Practices
1. Always use the centralized components from `@sonr.io/ui`
2. Prefer `cn()` utility for dynamic className composition
3. Leverage TypeScript for type-safe component usage
4. Use CSS variables for theming consistency
<Callout type="tip">
Remember: Our UI library is designed to be both flexible and consistent. When
in doubt, refer to the components in `@sonr.io/ui`.
</Callout>
-173
View File
@@ -1,173 +0,0 @@
---
title: "@sonr.io/ui"
sidebarTitle: "@sonr.io/ui"
description: "A package for Sonr's centralized shadcn/ui component library"
icon: "palette"
---
# @sonr.io/ui
## Overview
The `@sonr.io/ui` package serves as our centralized shadcn/ui component library, providing a consistent and accessible design system across Sonr's ecosystem. Leveraging the power of shadcn/ui, we've created a fully customizable and type-safe component library with a primary color of `#17c2ff`.
<Callout type="info">
Our UI package is built to provide maximum flexibility while maintaining
strict design consistency.
</Callout>
## Component Migration Strategy
### Before: Custom Button Component
Previously, our Button component relied on manual variant classes and custom implementations:
```tsx
// Old Implementation
const Button = ({ variant, className, ...props }) => {
const variantClasses = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
// Multiple manual variant definitions
};
return (
<button className={`${variantClasses[variant]} ${className}`} {...props} />
);
};
```
### After: Shadcn Button Implementation
Our new implementation uses `cva` (class-variance-authority) and the `cn` utility for robust variant management:
```tsx
// New Implementation
import { buttonVariants } from "@sonr.io/ui/components/ui/button";
import { cn } from "@sonr.io/ui/lib/utils";
const Button = ({
variant = "default",
size = "default",
className,
...props
}) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
};
```
<Callout type="success">
Key Improvements: - Type-safe variant management - Enhanced accessibility -
Consistent theming - Reduced bundle size
</Callout>
## Theme Configuration
### Color System
Our primary theme color is `#17c2ff`, defined in HSL format for maximum flexibility:
```css
:root {
--primary-h: 200; /* Hue */
--primary-s: 100%; /* Saturation */
--primary-l: 59%; /* Lightness */
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
}
```
### CSS Variables Structure
```css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 200 100% 59%;
--primary-foreground: 210 40% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
/* Additional theme variables */
}
```
## Usage Guide
### Importing Components
Import components directly from the `@sonr.io/ui` package:
```tsx
import { Button } from "@sonr.io/ui/components/ui/button";
import { Input } from "@sonr.io/ui/components/ui/input";
```
### Adding New Components
Use the shadcn CLI within the `packages/ui` directory:
```bash
# Navigate to packages/ui
cd packages/ui
# Add a new component
npx shadcn-ui@latest add button
```
<Callout type="warning">
Always add components from the `packages/ui` directory to maintain our
centralized component management.
</Callout>
## Architecture Patterns
### Monorepo Structure
```
sonr/
├── packages/
│ └── ui/
│ ├── components/
│ │ └── ui/
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── ...
│ ├── lib/
│ │ └── utils.ts
│ └── styles/
│ └── globals.css
```
### Key Principles
- **Single Source of Truth**: All UI components live in `packages/ui`
- **No Local `components.json`**: Centralized configuration
- **Turbo-powered Build Pipeline**: Efficient component compilation
## Global Styles Integration
In your application's main entry point:
```tsx
import "@sonr.io/ui/styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
```
## Best Practices
1. Always use the centralized components from `@sonr.io/ui`
2. Prefer `cn()` utility for dynamic className composition
3. Leverage TypeScript for type-safe component usage
4. Use CSS variables for theming consistency
<Callout type="tip">
Remember: Our UI library is designed to be both flexible and consistent. When
in doubt, refer to the components in `@sonr.io/ui`.
</Callout>
-173
View File
@@ -1,173 +0,0 @@
---
title: "@sonr.io/ui"
sidebarTitle: "@sonr.io/ui"
description: "A package for Sonr's centralized shadcn/ui component library"
icon: "palette"
---
# @sonr.io/ui
## Overview
The `@sonr.io/ui` package serves as our centralized shadcn/ui component library, providing a consistent and accessible design system across Sonr's ecosystem. Leveraging the power of shadcn/ui, we've created a fully customizable and type-safe component library with a primary color of `#17c2ff`.
<Callout type="info">
Our UI package is built to provide maximum flexibility while maintaining
strict design consistency.
</Callout>
## Component Migration Strategy
### Before: Custom Button Component
Previously, our Button component relied on manual variant classes and custom implementations:
```tsx
// Old Implementation
const Button = ({ variant, className, ...props }) => {
const variantClasses = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
// Multiple manual variant definitions
};
return (
<button className={`${variantClasses[variant]} ${className}`} {...props} />
);
};
```
### After: Shadcn Button Implementation
Our new implementation uses `cva` (class-variance-authority) and the `cn` utility for robust variant management:
```tsx
// New Implementation
import { buttonVariants } from "@sonr.io/ui/components/ui/button";
import { cn } from "@sonr.io/ui/lib/utils";
const Button = ({
variant = "default",
size = "default",
className,
...props
}) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
};
```
<Callout type="success">
Key Improvements: - Type-safe variant management - Enhanced accessibility -
Consistent theming - Reduced bundle size
</Callout>
## Theme Configuration
### Color System
Our primary theme color is `#17c2ff`, defined in HSL format for maximum flexibility:
```css
:root {
--primary-h: 200; /* Hue */
--primary-s: 100%; /* Saturation */
--primary-l: 59%; /* Lightness */
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
}
```
### CSS Variables Structure
```css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 200 100% 59%;
--primary-foreground: 210 40% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
/* Additional theme variables */
}
```
## Usage Guide
### Importing Components
Import components directly from the `@sonr.io/ui` package:
```tsx
import { Button } from "@sonr.io/ui/components/ui/button";
import { Input } from "@sonr.io/ui/components/ui/input";
```
### Adding New Components
Use the shadcn CLI within the `packages/ui` directory:
```bash
# Navigate to packages/ui
cd packages/ui
# Add a new component
npx shadcn-ui@latest add button
```
<Callout type="warning">
Always add components from the `packages/ui` directory to maintain our
centralized component management.
</Callout>
## Architecture Patterns
### Monorepo Structure
```
sonr/
├── packages/
│ └── ui/
│ ├── components/
│ │ └── ui/
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── ...
│ ├── lib/
│ │ └── utils.ts
│ └── styles/
│ └── globals.css
```
### Key Principles
- **Single Source of Truth**: All UI components live in `packages/ui`
- **No Local `components.json`**: Centralized configuration
- **Turbo-powered Build Pipeline**: Efficient component compilation
## Global Styles Integration
In your application's main entry point:
```tsx
import "@sonr.io/ui/styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
```
## Best Practices
1. Always use the centralized components from `@sonr.io/ui`
2. Prefer `cn()` utility for dynamic className composition
3. Leverage TypeScript for type-safe component usage
4. Use CSS variables for theming consistency
<Callout type="tip">
Remember: Our UI library is designed to be both flexible and consistent. When
in doubt, refer to the components in `@sonr.io/ui`.
</Callout>
-173
View File
@@ -1,173 +0,0 @@
---
title: "@sonr.io/ui"
sidebarTitle: "@sonr.io/ui"
description: "A package for Sonr's centralized shadcn/ui component library"
icon: "palette"
---
# @sonr.io/ui
## Overview
The `@sonr.io/ui` package serves as our centralized shadcn/ui component library, providing a consistent and accessible design system across Sonr's ecosystem. Leveraging the power of shadcn/ui, we've created a fully customizable and type-safe component library with a primary color of `#17c2ff`.
<Callout type="info">
Our UI package is built to provide maximum flexibility while maintaining
strict design consistency.
</Callout>
## Component Migration Strategy
### Before: Custom Button Component
Previously, our Button component relied on manual variant classes and custom implementations:
```tsx
// Old Implementation
const Button = ({ variant, className, ...props }) => {
const variantClasses = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
// Multiple manual variant definitions
};
return (
<button className={`${variantClasses[variant]} ${className}`} {...props} />
);
};
```
### After: Shadcn Button Implementation
Our new implementation uses `cva` (class-variance-authority) and the `cn` utility for robust variant management:
```tsx
// New Implementation
import { buttonVariants } from "@sonr.io/ui/components/ui/button";
import { cn } from "@sonr.io/ui/lib/utils";
const Button = ({
variant = "default",
size = "default",
className,
...props
}) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
};
```
<Callout type="success">
Key Improvements: - Type-safe variant management - Enhanced accessibility -
Consistent theming - Reduced bundle size
</Callout>
## Theme Configuration
### Color System
Our primary theme color is `#17c2ff`, defined in HSL format for maximum flexibility:
```css
:root {
--primary-h: 200; /* Hue */
--primary-s: 100%; /* Saturation */
--primary-l: 59%; /* Lightness */
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
}
```
### CSS Variables Structure
```css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 200 100% 59%;
--primary-foreground: 210 40% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
/* Additional theme variables */
}
```
## Usage Guide
### Importing Components
Import components directly from the `@sonr.io/ui` package:
```tsx
import { Button } from "@sonr.io/ui/components/ui/button";
import { Input } from "@sonr.io/ui/components/ui/input";
```
### Adding New Components
Use the shadcn CLI within the `packages/ui` directory:
```bash
# Navigate to packages/ui
cd packages/ui
# Add a new component
npx shadcn-ui@latest add button
```
<Callout type="warning">
Always add components from the `packages/ui` directory to maintain our
centralized component management.
</Callout>
## Architecture Patterns
### Monorepo Structure
```
sonr/
├── packages/
│ └── ui/
│ ├── components/
│ │ └── ui/
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── ...
│ ├── lib/
│ │ └── utils.ts
│ └── styles/
│ └── globals.css
```
### Key Principles
- **Single Source of Truth**: All UI components live in `packages/ui`
- **No Local `components.json`**: Centralized configuration
- **Turbo-powered Build Pipeline**: Efficient component compilation
## Global Styles Integration
In your application's main entry point:
```tsx
import "@sonr.io/ui/styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
```
## Best Practices
1. Always use the centralized components from `@sonr.io/ui`
2. Prefer `cn()` utility for dynamic className composition
3. Leverage TypeScript for type-safe component usage
4. Use CSS variables for theming consistency
<Callout type="tip">
Remember: Our UI library is designed to be both flexible and consistent. When
in doubt, refer to the components in `@sonr.io/ui`.
</Callout>
-173
View File
@@ -1,173 +0,0 @@
---
title: "@sonr.io/ui"
sidebarTitle: "@sonr.io/ui"
description: "A package for Sonr's centralized shadcn/ui component library"
icon: "palette"
---
# @sonr.io/ui
## Overview
The `@sonr.io/ui` package serves as our centralized shadcn/ui component library, providing a consistent and accessible design system across Sonr's ecosystem. Leveraging the power of shadcn/ui, we've created a fully customizable and type-safe component library with a primary color of `#17c2ff`.
<Callout type="info">
Our UI package is built to provide maximum flexibility while maintaining
strict design consistency.
</Callout>
## Component Migration Strategy
### Before: Custom Button Component
Previously, our Button component relied on manual variant classes and custom implementations:
```tsx
// Old Implementation
const Button = ({ variant, className, ...props }) => {
const variantClasses = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-black",
// Multiple manual variant definitions
};
return (
<button className={`${variantClasses[variant]} ${className}`} {...props} />
);
};
```
### After: Shadcn Button Implementation
Our new implementation uses `cva` (class-variance-authority) and the `cn` utility for robust variant management:
```tsx
// New Implementation
import { buttonVariants } from "@sonr.io/ui/components/ui/button";
import { cn } from "@sonr.io/ui/lib/utils";
const Button = ({
variant = "default",
size = "default",
className,
...props
}) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
};
```
<Callout type="success">
Key Improvements: - Type-safe variant management - Enhanced accessibility -
Consistent theming - Reduced bundle size
</Callout>
## Theme Configuration
### Color System
Our primary theme color is `#17c2ff`, defined in HSL format for maximum flexibility:
```css
:root {
--primary-h: 200; /* Hue */
--primary-s: 100%; /* Saturation */
--primary-l: 59%; /* Lightness */
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
}
```
### CSS Variables Structure
```css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 200 100% 59%;
--primary-foreground: 210 40% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
/* Additional theme variables */
}
```
## Usage Guide
### Importing Components
Import components directly from the `@sonr.io/ui` package:
```tsx
import { Button } from "@sonr.io/ui/components/ui/button";
import { Input } from "@sonr.io/ui/components/ui/input";
```
### Adding New Components
Use the shadcn CLI within the `packages/ui` directory:
```bash
# Navigate to packages/ui
cd packages/ui
# Add a new component
npx shadcn-ui@latest add button
```
<Callout type="warning">
Always add components from the `packages/ui` directory to maintain our
centralized component management.
</Callout>
## Architecture Patterns
### Monorepo Structure
```
sonr/
├── packages/
│ └── ui/
│ ├── components/
│ │ └── ui/
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── ...
│ ├── lib/
│ │ └── utils.ts
│ └── styles/
│ └── globals.css
```
### Key Principles
- **Single Source of Truth**: All UI components live in `packages/ui`
- **No Local `components.json`**: Centralized configuration
- **Turbo-powered Build Pipeline**: Efficient component compilation
## Global Styles Integration
In your application's main entry point:
```tsx
import "@sonr.io/ui/styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
```
## Best Practices
1. Always use the centralized components from `@sonr.io/ui`
2. Prefer `cn()` utility for dynamic className composition
3. Leverage TypeScript for type-safe component usage
4. Use CSS variables for theming consistency
<Callout type="tip">
Remember: Our UI library is designed to be both flexible and consistent. When
in doubt, refer to the components in `@sonr.io/ui`.
</Callout>
+17
View File
@@ -0,0 +1,17 @@
CHAIN_ID=sonrtest_1-1
DENOM=usnr
KEYRING=test
KEYALGO=eth_secp256k1
BLOCK_TIME=5s
VOTING_PERIOD=30s
EXPEDITED_VOTING_PERIOD=15s
MAX_GAS=100000000
MIN_COMMISSION_RATE=0.050000000000000000
NARUTO_MNEMONIC="decorate bright ozone fork gallery riot bus exhaust worth way bone indoor calm squirrel merry zero scheme cotton until shop any excess stage laundry"
SENKU_MNEMONIC="wealth flavor believe regret funny network recall kiss grape useless pepper cram hint member few certain unveil rather brick bargain curious require crowd raise"
YAEGER_MNEMONIC="quality vacuum heart guard buzz spike sight swarm shove special gym robust assume sudden deposit grid alcohol choice devote leader tilt noodle tide penalty"
FAUCET_MNEMONIC="notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"
BASE_ALLOCATION=100000000000000000000000000usnr
STAKE_AMOUNT=30000000000000000000000usnr
FAUCET_ALLOCATION=250000000000000000000000000000usnr
DOCKER_IMAGE=onsonr/snrd:latest
+106
View File
@@ -0,0 +1,106 @@
name: sonr-core
services:
ipfs:
container_name: ipfs
image: ipfs/kubo:release
ports:
- "127.0.0.1:5001:5001"
- "127.0.0.1:8080:8080"
- "127.0.0.1:4001:4001"
volumes:
- ${HOME}/.ipfs:/data/ipfs
networks:
- sonr-network
cluster:
container_name: cluster
image: ipfs/ipfs-cluster:latest
depends_on:
- ipfs
environment:
CLUSTER_PEERNAME: cluster
CLUSTER_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
CLUSTER_IPFSHTTP_NODEMULTIADDRESS: /dns4/ipfs/tcp/5001
CLUSTER_CRDT_TRUSTEDPEERS: "*"
CLUSTER_MONITORPINGINTERVAL: 2s
ports:
- "127.0.0.1:9094:9094"
- "127.0.0.1:9095:9095"
volumes:
- ${HOME}/.ipfs-cluster:/data/ipfs-cluster
networks:
- sonr-network
caddy:
container_name: caddy
image: caddy:2-alpine
restart: unless-stopped
depends_on:
snrd:
condition: service_healthy
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
environment:
CADDY_DOMAIN: ${CADDY_DOMAIN:-localhost}
networks:
- sonr-network
snrd:
container_name: snrd
image: onsonr/snrd:latest
build:
context: .
dockerfile: cmd/snrd/Dockerfile
restart: unless-stopped
depends_on:
- ipfs
command: ["/usr/bin/devnet"]
environment:
CHAIN_ID: ${CHAIN_ID:-sonrtest_1-1}
MONIKER: ${MONIKER:-sonr-dev-node}
DENOM: ${DENOM:-usnr}
BLOCK_TIME: ${BLOCK_TIME:-1s}
CLEAN: ${CLEAN:-true}
KEYRING: ${KEYRING:-test}
HOME_DIR: /home/snrd/.snrd
KEY: acc0
KEY2: acc1
KEYALGO: eth_secp256k1
LOG_LEVEL: ${LOG_LEVEL:-debug}
TRACE: ${TRACE:-true}
SKIP_INSTALL: "true"
ports:
- "26657:26657"
- "1317:1317"
- "9090:9090"
- "9091:9091"
- "8545:8545"
- "8546:8546"
- "26656:26656"
- "6060:6060"
volumes:
- ${HOME}/.sonr:/root/.sonr
healthcheck:
test: ["CMD", "snrd", "status", "--home", "/home/snrd/.snrd"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
networks:
- sonr-network
networks:
sonr-network:
driver: bridge
name: sonr-network
volumes:
caddy-config:
caddy-data:
+17
View File
@@ -0,0 +1,17 @@
CHAIN_ID=sonrtest_1-1
DENOM=usnr
KEYRING=test
KEYALGO=eth_secp256k1
BLOCK_TIME=5s
VOTING_PERIOD=30s
EXPEDITED_VOTING_PERIOD=15s
MAX_GAS=100000000
MIN_COMMISSION_RATE=0.050000000000000000
NARUTO_MNEMONIC="decorate bright ozone fork gallery riot bus exhaust worth way bone indoor calm squirrel merry zero scheme cotton until shop any excess stage laundry"
SENKU_MNEMONIC="wealth flavor believe regret funny network recall kiss grape useless pepper cram hint member few certain unveil rather brick bargain curious require crowd raise"
YAEGER_MNEMONIC="quality vacuum heart guard buzz spike sight swarm shove special gym robust assume sudden deposit grid alcohol choice devote leader tilt noodle tide penalty"
FAUCET_MNEMONIC="notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"
BASE_ALLOCATION=100000000000000000000000000usnr
STAKE_AMOUNT=30000000000000000000000usnr
FAUCET_ALLOCATION=250000000000000000000000000000usnr
DOCKER_IMAGE=onsonr/snrd:latest
+591
View File
@@ -0,0 +1,591 @@
# Docker Containers
This document describes all Docker containers used in the Sonr testnet architecture.
## Overview
The testnet consists of 7 containers organized in a validator-sentry architecture:
- **3 Validators** (private, isolated networks)
- **3 Sentry Nodes** (public-facing, Cloudflare tunnel integration)
- **1 IPFS Node** (distributed storage)
All containers use the `unless-stopped` restart policy for resilience.
---
## Container Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Cloudflare Tunnel (DockFlare) │
└────────────────────┬────────────────────────────────────┘
┌────────────────┴────────────────────┐
│ net-public + cloudflare-net │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐
│ │ sentry- │ │ sentry- │ │ sentry- │ │ IPFS │
│ │ alice │◄─► bob │◄─► carol │ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┘
└───────┼─────────────┼─────────────┼─────────────────┘
│ │ │
│ private │ private │ private
│ │ │
┌───────▼──────┐ ┌────▼──────┐ ┌────▼──────┐
│ val-alice │ │ val-bob │ │ val-carol │
│ (net-alice) │ │ (net-bob) │ │(net-carol)│
└──────────────┘ └───────────┘ └───────────┘
```
---
## Validator Containers
Validators are the core consensus nodes that produce blocks and maintain blockchain state. They are isolated on private networks for security.
### val-alice
**Purpose:** Alice's validator node
**Image:** `onsonr/snrd:latest`
**Network:** `net-alice` (private)
**Volume:** `./val-alice:/root/.sonr`
**Configuration:**
- **Chain ID:** `sonrtest_1-1`
- **Moniker:** `val-alice`
- **Pruning:** Disabled (`--pruning=nothing`)
- **Min Gas Prices:** `0usnr` (free for testnet)
**Security Features:**
- Runs on isolated private network
- Only connects to its own sentry (`sentry-alice`)
- No direct public access
- Protected by sentry's `private_peer_ids` configuration
**Data Directory:**
```
./val-alice/
├── config/
│ ├── genesis.json # Genesis state
│ ├── config.toml # Tendermint config
│ ├── app.toml # Application config
│ └── priv_validator_key.json # Validator signing key (CRITICAL!)
└── data/ # Blockchain data
```
---
### val-bob
**Purpose:** Bob's validator node
**Image:** `onsonr/snrd:latest`
**Network:** `net-bob` (private)
**Volume:** `./val-bob:/root/.sonr`
**Configuration:**
- **Chain ID:** `sonrtest_1-1`
- **Moniker:** `val-bob`
- **Pruning:** Disabled
- **Min Gas Prices:** `0usnr`
**Security:** Same as val-alice, isolated on `net-bob`
---
### val-carol
**Purpose:** Carol's validator node
**Image:** `onsonr/snrd:latest`
**Network:** `net-carol` (private)
**Volume:** `./val-carol:/root/.sonr`
**Configuration:**
- **Chain ID:** `sonrtest_1-1`
- **Moniker:** `val-carol`
- **Pruning:** Disabled
- **Min Gas Prices:** `0usnr`
**Security:** Same as val-alice, isolated on `net-carol`
---
## Sentry Containers
Sentry nodes act as a protective layer between validators and the public internet. They handle all public RPC, REST, gRPC, and EVM requests while shielding validator identities.
### sentry-alice
**Purpose:** Alice's public-facing sentry node
**Image:** `onsonr/snrd:latest`
**Networks:**
- `net-alice` (connects to val-alice)
- `net-public` (connects to other sentries)
- `cloudflare-net` (for DockFlare tunnels)
**Volume:** `./sentry-alice:/root/.sonr`
**Depends On:** `val-alice`
**Configuration:**
- **Chain ID:** `sonrtest_1-1`
- **Moniker:** `sentry-alice`
- **Pruning:** Disabled
- **Min Gas Prices:** `0usnr`
**Exposed Services:**
- **RPC:** Port 26657 (Tendermint RPC)
- **REST:** Port 1317 (Cosmos REST API)
- **gRPC:** Port 9090 (gRPC endpoint)
- **EVM JSON-RPC:** Port 8545 (Ethereum-compatible)
- **EVM WebSocket:** Port 8546 (WebSocket subscriptions)
**JSON-RPC APIs Enabled:**
- `eth` - Ethereum JSON-RPC
- `txpool` - Transaction pool inspection
- `personal` - Account management
- `net` - Network info
- `debug` - Debugging APIs
- `web3` - Web3 utilities
**Cloudflare Tunnel Endpoints:**
| Endpoint | Hostname | Internal Service | Description |
|----------|----------|------------------|-------------|
| RPC | `alice-rpc.sonr.land` | `http://sentry-alice:26657` | Tendermint RPC for queries and transactions |
| REST | `alice-rest.sonr.land` | `http://sentry-alice:1317` | Cosmos REST API (LCD) |
| gRPC | `alice-grpc.sonr.land` | `http://sentry-alice:9090` | gRPC for efficient binary communication |
| EVM | `alice-evm.sonr.land` | `http://sentry-alice:8545` | EVM JSON-RPC (MetaMask compatible) |
**Peer Connections:**
- **Persistent Peer:** `val-alice` (private connection)
- **Seeds:** `sentry-bob`, `sentry-carol` (public P2P)
- **Private Peer IDs:** Marks `val-alice` as private to hide from network
**Security Features:**
- Shields validator from direct exposure
- Handles all public-facing traffic
- Rate limiting and DDoS protection via Cloudflare
- TLS encryption via Cloudflare tunnels
---
### sentry-bob
**Purpose:** Bob's public-facing sentry node
**Image:** `onsonr/snrd:latest`
**Networks:** `net-bob`, `net-public`, `cloudflare-net`
**Volume:** `./sentry-bob:/root/.sonr`
**Depends On:** `val-bob`
**Configuration:** Same as sentry-alice, with moniker `sentry-bob`
**Cloudflare Tunnel Endpoints:**
- `bob-rpc.sonr.land` → RPC (26657)
- `bob-rest.sonr.land` → REST (1317)
- `bob-grpc.sonr.land` → gRPC (9090)
- `bob-evm.sonr.land` → EVM JSON-RPC (8545)
**Peer Connections:**
- **Persistent Peer:** `val-bob`
- **Seeds:** `sentry-alice`, `sentry-carol`
---
### sentry-carol
**Purpose:** Carol's public-facing sentry node
**Image:** `onsonr/snrd:latest`
**Networks:** `net-carol`, `net-public`, `cloudflare-net`
**Volume:** `./sentry-carol:/root/.sonr`
**Depends On:** `val-carol`
**Configuration:** Same as sentry-alice, with moniker `sentry-carol`
**Cloudflare Tunnel Endpoints:**
- `carol-rpc.sonr.land` → RPC (26657)
- `carol-rest.sonr.land` → REST (1317)
- `carol-grpc.sonr.land` → gRPC (9090)
- `carol-evm.sonr.land` → EVM JSON-RPC (8545)
**Peer Connections:**
- **Persistent Peer:** `val-carol`
- **Seeds:** `sentry-alice`, `sentry-bob`
---
## IPFS Container
### ipfsctl
**Purpose:** Distributed file storage and content addressing
**Image:** `ipfs/kubo:latest` (official IPFS implementation)
**Networks:**
- `net-public` (connects to sentry nodes)
- `cloudflare-net` (for DockFlare tunnels)
**Volume:** `ipfs-data:/data/ipfs` (named volume)
**Configuration:**
- **IPFS Profile:** `server` (optimized for server deployment)
**Exposed Services:**
- **API:** Port 5001 (IPFS HTTP API)
- **Gateway:** Port 8080 (IPFS Gateway for content retrieval)
- **Swarm:** Port 4001 (P2P networking)
**Cloudflare Tunnel Endpoints:**
| Endpoint | Hostname | Internal Service | Description |
|----------|----------|------------------|-------------|
| API | `ipfs-api.sonr.land` | `http://ipfsctl:5001` | IPFS API for adding/pinning content |
| Gateway | `ipfs-gateway.sonr.land` | `http://ipfsctl:8080` | HTTP gateway for retrieving content |
**Features:**
- **Content-Addressed Storage:** Files identified by cryptographic hash (CID)
- **Distributed Network:** Connects to global IPFS network
- **Persistent Storage:** Data stored in named volume
- **HTTP API:** RESTful API for programmatic access
- **Gateway Access:** Retrieve content via HTTP using CID
**Common Operations:**
```bash
# Add file to IPFS
curl -X POST -F file=@myfile.txt https://ipfs-api.sonr.land/api/v0/add
# Retrieve file by CID
curl https://ipfs-gateway.sonr.land/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme
# Check version
curl -X POST https://ipfs-api.sonr.land/api/v0/version | jq
```
---
## Network Topology
### Private Networks
Each validator has its own isolated network:
| Network | Purpose | Containers |
|---------|---------|------------|
| `net-alice` | Alice validator isolation | `val-alice`, `sentry-alice` |
| `net-bob` | Bob validator isolation | `val-bob`, `sentry-bob` |
| `net-carol` | Carol validator isolation | `val-carol`, `sentry-carol` |
**Security Benefits:**
- Validators cannot directly communicate with each other
- Prevents Byzantine attacks at network layer
- Forces all communication through sentries
### Public Network
| Network | Purpose | Containers |
|---------|---------|------------|
| `net-public` | Sentry interconnection | `sentry-alice`, `sentry-bob`, `sentry-carol`, `ipfsctl` |
**Purpose:**
- Sentries discover and connect to each other
- IPFS accessible to all sentries
- Public P2P gossip network
### Cloudflare Network
| Network | Purpose | Containers |
|---------|---------|------------|
| `cloudflare-net` | DockFlare tunnel access | `sentry-alice`, `sentry-bob`, `sentry-carol`, `ipfsctl` |
**Type:** External (must be created before starting stack)
**Purpose:** Allows DockFlare to discover and tunnel traffic to containers
**Setup:**
```bash
docker network create cloudflare-net
```
---
## Volume Management
### Bind Mounts (Validators & Sentries)
```
./val-alice:/root/.sonr
./val-bob:/root/.sonr
./val-carol:/root/.sonr
./sentry-alice:/root/.sonr
./sentry-bob:/root/.sonr
./sentry-carol:/root/.sonr
```
**Advantages:**
- Direct file system access from host
- Easy backup and inspection
- No permission issues (files owned by host user)
**Backup:**
```bash
tar -czf testnet-backup.tar.gz val-* sentry-*
```
### Named Volume (IPFS)
```
ipfs-data:/data/ipfs
```
**Advantages:**
- Managed by Docker
- Better performance on some systems
- Automatic cleanup with `docker compose down -v`
**Backup:**
```bash
docker run --rm -v ipfs-data:/data -v $(pwd):/backup alpine tar czf /backup/ipfs-backup.tar.gz /data
```
---
## Container Management
### Start All Containers
```bash
make start
# or
docker compose up -d
```
### Stop All Containers
```bash
make stop
# or
docker compose down
```
### View Container Logs
```bash
# All containers
docker compose logs -f
# Specific container
docker compose logs -f sentry-alice
# Last 100 lines
docker compose logs --tail=100 val-alice
```
### Execute Commands in Containers
```bash
# Via bootstrap script
devbox run exec sentry-alice status
# Via docker compose
docker compose exec sentry-alice snrd status --home /root/.sonr
# Via docker
docker exec -it sentry-alice snrd keys list --keyring-backend test
```
### Inspect Container Configuration
```bash
# View environment variables
docker inspect sentry-alice -f '{{json .Config.Env}}' | jq
# View networks
docker inspect sentry-alice -f '{{json .NetworkSettings.Networks}}' | jq
# View mounts
docker inspect sentry-alice -f '{{json .Mounts}}' | jq
```
### Resource Usage
```bash
# Real-time stats
docker stats
# Container resource limits (if set)
docker inspect sentry-alice -f '{{json .HostConfig.Memory}}' | jq
```
---
## Health Checks
### Container Status
```bash
docker ps --filter "name=val-" --filter "name=sentry-" --filter "name=ipfs"
```
### Node Sync Status
```bash
# Check if nodes are syncing
for container in sentry-alice sentry-bob sentry-carol; do
echo "=== $container ==="
docker exec $container sh -c 'curl -s http://localhost:26657/status | jq -r ".result.sync_info.catching_up"'
done
```
### Block Height
```bash
# Current block height
docker exec sentry-alice sh -c 'curl -s http://localhost:26657/status | jq -r ".result.sync_info.latest_block_height"'
```
### IPFS Health
```bash
# Check IPFS daemon
curl -X POST https://ipfs-api.sonr.land/api/v0/version | jq
# Check connected peers
curl -X POST https://ipfs-api.sonr.land/api/v0/swarm/peers | jq
```
---
## Security Considerations
### Validator Security
1. **Never expose validators directly** - Always use sentries
2. **Backup `priv_validator_key.json`** - Cannot be recovered if lost
3. **Monitor validator uptime** - Downtime results in slashing
4. **Use KMS in production** - Hardware security modules for key management
### Sentry Security
1. **Rate limiting via Cloudflare** - Protects against DDoS
2. **TLS encryption** - All traffic encrypted via Cloudflare tunnels
3. **No exposed ports** - All access via tunnels only
4. **Regular updates** - Keep `onsonr/snrd` image updated
### IPFS Security
1. **Content verification** - All content verified by CID
2. **No private data** - IPFS is a public network
3. **Pin important content** - Prevent garbage collection
4. **Monitor storage** - IPFS can grow large over time
---
## Troubleshooting
### Container Won't Start
```bash
# Check logs
docker compose logs <container-name>
# Check resource usage
docker stats
# Verify networks exist
docker network ls | grep -E "net-|cloudflare"
# Recreate container
docker compose up -d --force-recreate <container-name>
```
### Permission Issues
```bash
# Fix ownership (if needed)
sudo chown -R $USER:$USER val-* sentry-*
# Check bind mount permissions
ls -la val-alice/
```
### Network Connectivity Issues
```bash
# Test connectivity between containers
docker exec sentry-alice ping -c 3 val-alice
docker exec sentry-alice ping -c 3 sentry-bob
# Check network configuration
docker network inspect net-public
```
### IPFS Issues
```bash
# Check IPFS daemon status
docker compose logs ipfsctl
# Restart IPFS
docker compose restart ipfsctl
# Clear IPFS cache (WARNING: destructive)
docker compose down
docker volume rm testnet_ipfs-data
docker compose up -d
```
---
## Performance Tuning
### Resource Limits (Optional)
Add to `docker-compose.yml`:
```yaml
services:
val-alice:
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
reservations:
cpus: '1.0'
memory: 2G
```
### Logging Configuration
Prevent log bloat:
```yaml
services:
val-alice:
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
```
---
## Maintenance
### Update Containers
```bash
# Pull latest images
docker compose pull
# Restart with new images
docker compose up -d
```
### Clean Up
```bash
# Remove stopped containers
docker compose down
# Remove all data (WARNING: destructive)
docker compose down -v
# Clean Docker system
docker system prune -a
```
### Backup Procedure
```bash
# Stop containers
make stop
# Backup data
tar -czf testnet-backup-$(date +%Y%m%d).tar.gz val-* sentry-* .env
# Backup IPFS volume
docker run --rm -v testnet_ipfs-data:/data -v $(pwd):/backup \
alpine tar czf /backup/ipfs-backup-$(date +%Y%m%d).tar.gz /data
# Restart containers
make start
```
+247
View File
@@ -0,0 +1,247 @@
# Environment Variables
This document describes all environment variables that can be configured for the Sonr testnet.
## Configuration File
All variables are defined in `.env` file in the repository root. Copy `.env.example` to `.env` and customize:
```bash
cp .env.example .env
```
---
## Global Configuration
These variables are used during initialization and apply to all services.
### Chain Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `CHAIN_ID` | `sonrtest_1-1` | Blockchain chain identifier |
| `DENOM` | `usnr` | Base denomination for the native token |
| `KEYRING` | `test` | Keyring backend (test, file, os) |
| `KEYALGO` | `eth_secp256k1` | Key algorithm for validator keys |
### Network Parameters
| Variable | Default | Description |
|----------|---------|-------------|
| `BLOCK_TIME` | `5s` | Target block time |
| `VOTING_PERIOD` | `30s` | Governance proposal voting period |
| `EXPEDITED_VOTING_PERIOD` | `15s` | Expedited proposal voting period |
| `MAX_GAS` | `100000000` | Maximum gas per block |
| `MIN_COMMISSION_RATE` | `0.050000000000000000` | Minimum validator commission rate (5%) |
### Validator Mnemonics
**⚠️ WARNING**: Change these for production! Default mnemonics are public.
| Variable | Description |
|----------|-------------|
| `ALICE_MNEMONIC` | 24-word mnemonic for Alice validator |
| `BOB_MNEMONIC` | 24-word mnemonic for Bob validator |
| `CAROL_MNEMONIC` | 24-word mnemonic for Carol validator |
| `FAUCET_MNEMONIC` | 24-word mnemonic for faucet account |
### Initial Balances
| Variable | Default | Description |
|----------|---------|-------------|
| `BASE_ALLOCATION` | `100000000000000000000000000usnr` | Base allocation per validator (100M SNR) |
| `STAKE_AMOUNT` | `30000000000000000000000usnr` | Initial stake per validator (30M SNR) |
| `FAUCET_ALLOCATION` | `250000000000000000000000000000usnr` | Faucet account balance (250M SNR) |
### Docker Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `DOCKER_IMAGE` | `onsonr/snrd:latest` | Docker image for validator and sentry nodes |
---
## Container-Specific Variables
These variables are set in `docker-compose.yml` for each container.
### Validators (val-alice, val-bob, val-carol)
All validators share the same environment variables but with different values:
| Variable | Value | Description |
|----------|-------|-------------|
| `CHAIN_ID` | `sonrtest_1-1` | Chain identifier (same for all) |
| `MONIKER` | `val-{name}` | Validator node name (val-alice, val-bob, val-carol) |
**Command-line flags:**
- `--home /root/.sonr` - Node home directory
- `--pruning=nothing` - Disable state pruning
- `--minimum-gas-prices=0usnr` - Minimum gas price (0 for testnet)
- `--chain-id=sonrtest_1-1` - Chain identifier
**Networks:**
- Private network: `net-{name}` (net-alice, net-bob, net-carol)
**Volumes:**
- `./val-{name}:/root/.sonr` - Bind mount for node data
---
### Sentries (sentry-alice, sentry-bob, sentry-carol)
All sentries share the same environment variables but with different values:
| Variable | Value | Description |
|----------|-------|-------------|
| `CHAIN_ID` | `sonrtest_1-1` | Chain identifier (same for all) |
| `MONIKER` | `sentry-{name}` | Sentry node name (sentry-alice, sentry-bob, sentry-carol) |
**Command-line flags:**
- `--home /root/.sonr` - Node home directory
- `--pruning=nothing` - Disable state pruning
- `--minimum-gas-prices=0usnr` - Minimum gas price (0 for testnet)
- `--rpc.laddr=tcp://0.0.0.0:26657` - RPC listen address
- `--json-rpc.api=eth,txpool,personal,net,debug,web3` - Enabled JSON-RPC APIs
- `--json-rpc.address=0.0.0.0:8545` - EVM JSON-RPC address
- `--json-rpc.ws-address=0.0.0.0:8546` - EVM WebSocket address
- `--chain-id=sonrtest_1-1` - Chain identifier
**Networks:**
- Private network: `net-{name}` (connects to validator)
- `net-public` (connects to other sentries)
- `cloudflare-net` (for DockFlare tunnels)
**Volumes:**
- `./sentry-{name}:/root/.sonr` - Bind mount for node data
**DockFlare Labels (indexed):**
Each sentry exposes 4 endpoints via Cloudflare tunnels:
| Index | Hostname Pattern | Service | Description |
|-------|------------------|---------|-------------|
| `0` | `{name}-rpc.sonr.land` | `http://sentry-{name}:26657` | Tendermint RPC |
| `1` | `{name}-rest.sonr.land` | `http://sentry-{name}:1317` | Cosmos REST API |
| `2` | `{name}-grpc.sonr.land` | `http://sentry-{name}:9090` | gRPC endpoint |
| `3` | `{name}-evm.sonr.land` | `http://sentry-{name}:8545` | EVM JSON-RPC |
**Example for sentry-alice:**
```yaml
labels:
- "dockflare.enable=true"
- "dockflare.0.hostname=alice-rpc.sonr.land"
- "dockflare.0.service=http://sentry-alice:26657"
- "dockflare.1.hostname=alice-rest.sonr.land"
- "dockflare.1.service=http://sentry-alice:1317"
- "dockflare.2.hostname=alice-grpc.sonr.land"
- "dockflare.2.service=http://sentry-alice:9090"
- "dockflare.3.hostname=alice-evm.sonr.land"
- "dockflare.3.service=http://sentry-alice:8545"
```
---
### IPFS (ipfsctl)
| Variable | Value | Description |
|----------|-------|-------------|
| `IPFS_PROFILE` | `server` | IPFS configuration profile for server mode |
**Networks:**
- `net-public` (connects to sentries)
- `cloudflare-net` (for DockFlare tunnels)
**Volumes:**
- `ipfs-data:/data/ipfs` - Named volume for IPFS data
**DockFlare Labels:**
| Index | Hostname | Service | Description |
|-------|----------|---------|-------------|
| `0` | `ipfs-api.sonr.land` | `http://ipfsctl:5001` | IPFS API endpoint |
| `1` | `ipfs-gateway.sonr.land` | `http://ipfsctl:8080` | IPFS Gateway |
---
## DockFlare Configuration
DockFlare requires the following environment variables (not in `.env`, set when running DockFlare container):
| Variable | Required | Description |
|----------|----------|-------------|
| `CLOUDFLARE_API_TOKEN` | ✅ Yes | Cloudflare API token with Tunnel:Edit, DNS:Edit, Zone:Read permissions |
| `CLOUDFLARE_ACCOUNT_ID` | ⚠️ Recommended | Cloudflare account ID (found in dashboard) |
| `CLOUDFLARE_ZONE_ID` | ⚠️ Recommended | Cloudflare zone ID for domain (found in dashboard) |
**Example DockFlare Setup:**
```bash
docker run -d \
--name dockflare \
--restart unless-stopped \
--network cloudflare-net \
-v /var/run/docker.sock:/var/run/docker.sock \
-e CLOUDFLARE_API_TOKEN=your_token_here \
-e CLOUDFLARE_ACCOUNT_ID=your_account_id \
-e CLOUDFLARE_ZONE_ID=your_zone_id \
ghcr.io/sonr-io/dockflare:latest
```
---
## Runtime Overrides
You can override specific values at runtime by modifying `docker-compose.yml` or passing environment variables:
### Override Docker Image
```bash
export DOCKER_IMAGE=onsonr/snrd:v1.2.3
make start
```
### Override Chain ID
Edit `docker-compose.yml` and change all instances of:
```yaml
environment:
- CHAIN_ID=your-custom-chain-id
```
And update command flags:
```yaml
command: >
snrd start
...
--chain-id=your-custom-chain-id
```
---
## Security Best Practices
1. **Never commit `.env` to version control** - It's already in `.gitignore`
2. **Generate new mnemonics for production**:
```bash
snrd keys add test --keyring-backend test --output json | jq -r .mnemonic
```
3. **Use KMS for production validator keys** (e.g., tmkms)
4. **Rotate Cloudflare API tokens** regularly
5. **Use strong passwords** if switching from `test` keyring to `file` or `os`
6. **Backup mnemonics securely** - They cannot be recovered if lost
---
## Verification
After configuration, verify your setup:
```bash
# Check .env is loaded
make status
# Test endpoints
make test
# View container environment
docker inspect sentry-alice -f '{{json .Config.Env}}' | jq
```
+66
View File
@@ -0,0 +1,66 @@
# Sonr Testnet Makefile
.PHONY: help all init start stop restart clean status logs test setup
# Default target
.DEFAULT_GOAL := help
# Help target
help:
@echo "Sonr Testnet Management (via Devbox)"
@echo ""
@echo "Setup:"
@echo " make all - Check/install devbox"
@echo ""
@echo "Testnet Operations:"
@echo " make setup - Create .env from template"
@echo " make init - Initialize validators and sentries"
@echo " make start - Start testnet"
@echo " make stop - Stop testnet"
@echo " make restart - Restart testnet"
@echo " make clean - Clean all data"
@echo " make status - Show status"
@echo " make logs - View logs"
@echo ""
@echo "Testing:"
@echo " make test - Run basic tests"
# Check/install devbox
all:
@command -v devbox >/dev/null 2>&1 || (echo "Installing devbox..." && curl -fsSL https://get.jetpack.io/devbox | bash)
@echo "✅ devbox is ready"
# Setup .env file
setup:
@devbox run setup
# Initialize validators
init:
@devbox run init
# Start testnet
start:
@devbox run start
# Stop testnet
stop:
@devbox run stop
# Restart testnet
restart:
@devbox run restart
# Clean all data
clean:
@devbox run clean
# Show status
status:
@devbox run status
# View logs
logs:
@devbox run logs
# Run tests
test:
@devbox run test
+407
View File
@@ -0,0 +1,407 @@
# Sonr Testnet
Production-ready 3-validator testnet with validator-sentry architecture and Cloudflare tunnel integration via DockFlare.
## Overview
This testnet provides:
- **3 Validators** with isolated private networks
- **3 Sentry Nodes** for public access and DDoS protection
- **IPFS Node** for distributed storage
- **Cloudflare Tunnels** for secure, zero-configuration public endpoints
- **14 Public Endpoints** via `*.sonr.land` domains
## Quick Start
### Prerequisites
1. **Docker & Docker Compose**
```bash
docker --version
docker compose version
```
2. **Devbox** (provides snrd binary via Nix)
```bash
# Auto-install via Makefile
make all
# Or install manually
curl -fsSL https://get.jetpack.io/devbox | bash
```
3. **DockFlare** (for Cloudflare tunnels)
```bash
docker network create cloudflare-net
docker run -d \
--name dockflare \
--restart unless-stopped \
--network cloudflare-net \
-v /var/run/docker.sock:/var/run/docker.sock \
-e CLOUDFLARE_API_TOKEN=your_token_here \
-e CLOUDFLARE_ACCOUNT_ID=your_account_id \
-e CLOUDFLARE_ZONE_ID=your_zone_id \
ghcr.io/sonr-io/dockflare:latest
```
> Get credentials from: https://dash.cloudflare.com
### Setup
```bash
# 1. Clone repository
git clone https://github.com/sonr-io/testnet
cd testnet
# 2. Install devbox (if not already installed)
make all
# 3. Create environment configuration
make setup
# Optional: Edit .env to customize
# 4. Initialize testnet
make init
# 5. Start testnet
make start
# 6. Verify endpoints
make test
```
**That's it!** Your testnet is running with Cloudflare tunnels.
---
## Commands
All testnet operations are managed via `make`:
```bash
make all # Check/install devbox
make setup # Create .env from template
make init # Initialize validators and sentries
make start # Start testnet
make stop # Stop testnet
make restart # Restart testnet
make clean # Clean all data (WARNING: destructive)
make status # Show status and endpoints
make logs # View logs
make test # Run basic tests
make help # Show available commands
```
> **Note:** All commands use devbox under the hood, which provides the `snrd` binary via Nix.
---
## Public Endpoints
All services are accessible via Cloudflare tunnels (no port conflicts):
### Sentry Endpoints
**Alice:**
- RPC: `https://alice-rpc.sonr.land`
- REST: `https://alice-rest.sonr.land`
- gRPC: `https://alice-grpc.sonr.land`
- EVM: `https://alice-evm.sonr.land`
**Bob:**
- RPC: `https://bob-rpc.sonr.land`
- REST: `https://bob-rest.sonr.land`
- gRPC: `https://bob-grpc.sonr.land`
- EVM: `https://bob-evm.sonr.land`
**Carol:**
- RPC: `https://carol-rpc.sonr.land`
- REST: `https://carol-rest.sonr.land`
- gRPC: `https://carol-grpc.sonr.land`
- EVM: `https://carol-evm.sonr.land`
### IPFS Endpoints
- API: `https://ipfs-api.sonr.land`
- Gateway: `https://ipfs-gateway.sonr.land`
### Example Usage
```bash
# Query blockchain status
curl https://alice-rpc.sonr.land/status | jq
# Query account balance
curl https://alice-rest.sonr.land/cosmos/bank/v1beta1/balances/idx16wx7ye3ce060tjvmmpu8lm0ak5xr7gm2vjyh4k | jq
# Send transaction
snrd tx bank send alice idx1... 1000000usnr \
--node https://alice-rpc.sonr.land \
--chain-id sonrtest_1-1 \
--keyring-backend test \
--yes
# IPFS operations
curl -X POST https://ipfs-api.sonr.land/api/v0/version | jq
curl https://ipfs-gateway.sonr.land/ipfs/<CID>
```
---
## Architecture
```
Cloudflare Tunnel (DockFlare)
┌──────────────────────────────────────────────┐
│ net-public + cloudflare-net │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ │sentry-alice │◄─►sentry-bob │◄─►sentry-carol │ │ IPFS │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────────┘
└─────────┼──────────────────┼──────────────────┼──────────────────────┘
│ │ │
│ Private │ Private │ Private
│ │ │
┌─────────▼────────┐ ┌──────▼────────┐ ┌─────▼─────────┐
│ val-alice │ │ val-bob │ │ val-carol │
│ (net-alice) │ │ (net-bob) │ │ (net-carol) │
└──────────────────┘ └───────────────┘ └───────────────┘
```
### Key Features
- **Validator Isolation**: Each validator on private network
- **Sentry Protection**: Public traffic filtered through sentries
- **No Port Conflicts**: All services via Cloudflare tunnels
- **Auto-Discovery**: DockFlare automatically creates tunnels
- **TLS Encryption**: All traffic encrypted via Cloudflare
---
## Genesis Accounts
| Name | Address | Balance | Purpose |
|------|---------|---------|---------|
| Alice | `idx140fehngcrxvhdt84x729p3f0qmkmea8n570lrg` | 100M SNR | Validator |
| Bob | `idx1r6yue0vuyj9m7xw78npspt9drq2tmtvgcrf7sr` | 100M SNR | Validator |
| Carol | `idx1pe9mc2q72u94sn2gg52ramrt26x5efw6kslflg` | 100M SNR | Validator |
| Faucet | `idx16wx7ye3ce060tjvmmpu8lm0ak5xr7gm2vjyh4k` | 250M SNR | Faucet |
**Total Genesis Supply:** 550M SNR (300M staked + 250M faucet)
---
## Configuration
Configuration is managed via `.env` file:
```bash
cp .env.example .env
```
Quick setup:
```bash
make setup # Creates .env from .env.example
```
**Key Variables:**
- `CHAIN_ID=sonrtest_1-1` - Chain identifier
- `DENOM=usnr` - Native token denomination
- `BLOCK_TIME=5s` - Target block time
- `VOTING_PERIOD=30s` - Governance voting period
- `ALICE_MNEMONIC`, `BOB_MNEMONIC`, `CAROL_MNEMONIC` - Validator keys
- `FAUCET_MNEMONIC` - Faucet account key
- `DOCKER_IMAGE=onsonr/snrd:latest` - Container image
**⚠️ WARNING:** Default mnemonics in `.env.example` are public. Generate new ones for production!
For complete configuration details, see [docs/Environment.md](docs/Environment.md)
---
## Documentation
### Detailed Guides
- **[Docker.md](docs/Docker.md)** - Complete container documentation
- All 7 containers explained
- Network topology and security
- Volume management
- Health checks and troubleshooting
- Performance tuning
- Backup procedures
- **[Cloudflare.md](docs/Cloudflare.md)** - DockFlare integration guide
- Complete domain mapping (14 endpoints)
- DockFlare setup and configuration
- DNS management
- Testing all endpoint types
- Access policies
- Troubleshooting tunnels
- **[Environment.md](docs/Environment.md)** - Environment variable reference
- Global configuration variables
- Container-specific variables
- DockFlare environment variables
- Runtime overrides
- Security best practices
- **[Architecture.md](docs/Architecture.md)** - Architecture deep dive
- **[CLAUDE.md](CLAUDE.md)** - Quick reference for AI assistants
---
## Troubleshooting
### Initialization Takes 1-2 Minutes
This is normal. The script initializes 6 nodes, creates genesis, and configures peer connections.
**Expected output:**
- 📋 Initializing validators
- 🛡️ Initializing sentries
- 💰 Adding genesis accounts
- 🔗 Setting up peer connections
- ✅ Completion with validator addresses
**If init fails:**
```bash
make clean
make init
```
### Validators Not Syncing
```bash
# Check logs
docker compose logs val-alice
# Verify peer connections
docker exec val-alice snrd tendermint show-node-id --home /root/.sonr
```
### Cloudflare Tunnels Not Working
Check DockFlare logs:
```bash
docker logs dockflare
```
Common issues:
- Missing `cloudflare-net` network
- Invalid API token
- Container not on `cloudflare-net`
See [docs/Cloudflare.md#troubleshooting](docs/Cloudflare.md#troubleshooting) for detailed help.
### IPFS Port Conflict
If IPFS fails with "port 8080 already allocated":
1. Stop the conflicting service
2. Or modify `docker-compose.yml` to use different port
---
## Production Deployment
**Before deploying to production:**
1. ✅ **Generate new mnemonics** - Never use defaults
```bash
snrd keys add test --keyring-backend test --output json | jq -r .mnemonic
```
2. ✅ **Use KMS** for validator key management (e.g., tmkms)
3. ✅ **Configure firewall rules** to restrict validator access
4. ✅ **Enable monitoring** (Prometheus, Grafana)
5. ✅ **Set up backups** of validator keys and state
```bash
tar -czf backup.tar.gz val-* sentry-* .env
```
6. ✅ **Rotate Cloudflare API tokens** regularly (every 90 days)
7. ✅ **Enable rate limiting** in Cloudflare dashboard
8. ✅ **Use persistent volumes** instead of bind mounts (optional)
---
## Development
### View Logs
```bash
# All services
make logs
# Specific service
docker compose logs -f sentry-alice
# Search logs
docker compose logs val-alice | grep -i error
```
### Execute Commands
```bash
# Via docker compose (recommended)
docker compose exec sentry-alice snrd query bank total
# Direct docker exec
docker exec -it sentry-alice snrd keys list --keyring-backend test
```
### Clean Restart
```bash
make clean # Remove all data (destructive!)
make init # Reinitialize
make start # Start fresh
```
---
## Testing
```bash
# Run basic tests
make test
```
### Manual Testing
```bash
# RPC
curl https://alice-rpc.sonr.land/status | jq
# REST
curl https://alice-rest.sonr.land/cosmos/base/tendermint/v1beta1/node_info | jq
# EVM JSON-RPC
curl -X POST https://alice-evm.sonr.land \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' | jq
# IPFS
curl -X POST https://ipfs-api.sonr.land/api/v0/version | jq
```
---
## Support
- **Documentation**: See [docs/](docs/) directory
- **Issues**: https://github.com/sonr-io/testnet/issues
- **Discord**: https://discord.gg/sonr
---
## License
Apache 2.0
+200
View File
@@ -0,0 +1,200 @@
name: sonr-testnet
networks:
net-naruto:
net-senku:
net-yaeger:
net-public:
dokploy-network:
external: true
services:
val-naruto:
image: onsonr/snrd:latest
container_name: val-naruto
command: sh -c "mkdir -p /root/.sonr && /usr/bin/testnet-setup.sh && snrd start --home /root/.sonr --pruning=nothing --minimum-gas-prices=0usnr --chain-id=sonrtest_1-1"
environment:
- CHAIN_ID=sonrtest_1-1
- MONIKER=val-naruto
- NODE_TYPE=validator
- VALIDATOR_NAME=val-naruto
- WAIT_FOR_SYNC=false
volumes:
- ../val-naruto:/root/.sonr
networks:
- net-naruto
restart: unless-stopped
sentry-naruto:
image: onsonr/snrd:latest
container_name: sentry-naruto
command: sh -c "/usr/bin/testnet-setup.sh && snrd start --home /root/.sonr --pruning=nothing --minimum-gas-prices=0usnr --rpc.laddr=tcp://0.0.0.0:26657 --json-rpc.api=eth,txpool,personal,net,debug,web3 --json-rpc.address=0.0.0.0:8545 --json-rpc.ws-address=0.0.0.0:8546 --chain-id=sonrtest_1-1"
environment:
- CHAIN_ID=sonrtest_1-1
- MONIKER=sentry-naruto
- NODE_TYPE=sentry
- VALIDATOR_NAME=sentry-naruto
- WAIT_FOR_SYNC=true
volumes:
- ../sentry-naruto:/root/.sonr
networks:
- net-naruto
- net-public
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.naruto-rpc.rule=Host(`${naruto_rpc_domain}`)
- traefik.http.routers.naruto-rpc.entrypoints=websecure
- traefik.http.routers.naruto-rpc.tls.certResolver=letsencrypt
- traefik.http.services.naruto-rpc.loadbalancer.server.port=26657
- traefik.http.routers.naruto-rest.rule=Host(`${naruto_rest_domain}`)
- traefik.http.routers.naruto-rest.entrypoints=websecure
- traefik.http.routers.naruto-rest.tls.certResolver=letsencrypt
- traefik.http.services.naruto-rest.loadbalancer.server.port=1317
- traefik.http.routers.naruto-grpc.rule=Host(`${naruto_grpc_domain}`)
- traefik.http.routers.naruto-grpc.entrypoints=websecure
- traefik.http.routers.naruto-grpc.tls.certResolver=letsencrypt
- traefik.http.services.naruto-grpc.loadbalancer.server.port=9090
- traefik.http.routers.naruto-evm.rule=Host(`${naruto_evm_domain}`)
- traefik.http.routers.naruto-evm.entrypoints=websecure
- traefik.http.routers.naruto-evm.tls.certResolver=letsencrypt
- traefik.http.services.naruto-evm.loadbalancer.server.port=8545
depends_on:
- val-naruto
restart: unless-stopped
# Senku's Validator
val-senku:
image: onsonr/snrd:latest
container_name: val-senku
command: sh -c "/usr/bin/testnet-setup.sh && snrd start --home /root/.sonr --pruning=nothing --minimum-gas-prices=0usnr --chain-id=sonrtest_1-1"
environment:
- CHAIN_ID=sonrtest_1-1
- MONIKER=val-senku
- NODE_TYPE=validator
- VALIDATOR_NAME=val-senku
- WAIT_FOR_SYNC=false
volumes:
- ../val-senku:/root/.sonr
networks:
- net-senku
restart: unless-stopped
# Senku's Sentry
sentry-senku:
image: onsonr/snrd:latest
container_name: sentry-senku
command: sh -c "/usr/bin/testnet-setup.sh && snrd start --home /root/.sonr --pruning=nothing --minimum-gas-prices=0usnr --rpc.laddr=tcp://0.0.0.0:26657 --json-rpc.api=eth,txpool,personal,net,debug,web3 --json-rpc.address=0.0.0.0:8545 --json-rpc.ws-address=0.0.0.0:8546 --chain-id=sonrtest_1-1"
environment:
- CHAIN_ID=sonrtest_1-1
- MONIKER=sentry-senku
- NODE_TYPE=sentry
- VALIDATOR_NAME=sentry-senku
- WAIT_FOR_SYNC=true
volumes:
- ../sentry-senku:/root/.sonr
networks:
- net-senku
- net-public
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.senku-rpc.rule=Host(`${senku_rpc_domain}`)
- traefik.http.routers.senku-rpc.entrypoints=websecure
- traefik.http.routers.senku-rpc.tls.certResolver=letsencrypt
- traefik.http.services.senku-rpc.loadbalancer.server.port=26657
- traefik.http.routers.senku-rest.rule=Host(`${senku_rest_domain}`)
- traefik.http.routers.senku-rest.entrypoints=websecure
- traefik.http.routers.senku-rest.tls.certResolver=letsencrypt
- traefik.http.services.senku-rest.loadbalancer.server.port=1317
- traefik.http.routers.senku-grpc.rule=Host(`${senku_grpc_domain}`)
- traefik.http.routers.senku-grpc.entrypoints=websecure
- traefik.http.routers.senku-grpc.tls.certResolver=letsencrypt
- traefik.http.services.senku-grpc.loadbalancer.server.port=9090
- traefik.http.routers.senku-evm.rule=Host(`${senku_evm_domain}`)
- traefik.http.routers.senku-evm.entrypoints=websecure
- traefik.http.routers.senku-evm.tls.certResolver=letsencrypt
- traefik.http.services.senku-evm.loadbalancer.server.port=8545
depends_on:
- val-senku
restart: unless-stopped
# Yaeger's Validator
val-yaeger:
image: onsonr/snrd:latest
container_name: val-yaeger
command: sh -c "/usr/bin/testnet-setup.sh && snrd start --home /root/.sonr --pruning=nothing --minimum-gas-prices=0usnr --chain-id=sonrtest_1-1"
environment:
- CHAIN_ID=sonrtest_1-1
- MONIKER=val-yaeger
- NODE_TYPE=validator
- VALIDATOR_NAME=val-yaeger
- WAIT_FOR_SYNC=false
volumes:
- ../val-yaeger:/root/.sonr
networks:
- net-yaeger
restart: unless-stopped
# Yaeger's Sentry (Public Node)
sentry-yaeger:
image: onsonr/snrd:latest
container_name: sentry-yaeger
command: sh -c "/usr/bin/testnet-setup.sh && snrd start --home /root/.sonr --pruning=nothing --minimum-gas-prices=0usnr --rpc.laddr=tcp://0.0.0.0:26657 --json-rpc.api=eth,txpool,personal,net,debug,web3 --json-rpc.address=0.0.0.0:8545 --json-rpc.ws-address=0.0.0.0:8546 --chain-id=sonrtest_1-1"
environment:
- CHAIN_ID=sonrtest_1-1
- MONIKER=sentry-yaeger
- NODE_TYPE=sentry
- VALIDATOR_NAME=sentry-yaeger
- WAIT_FOR_SYNC=true
volumes:
- ../sentry-yaeger:/root/.sonr
networks:
- net-yaeger
- net-public
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.yaeger-rpc.rule=Host(`${yaeger_rpc_domain}`)
- traefik.http.routers.yaeger-rpc.entrypoints=websecure
- traefik.http.routers.yaeger-rpc.tls.certResolver=letsencrypt
- traefik.http.services.yaeger-rpc.loadbalancer.server.port=26657
- traefik.http.routers.yaeger-rest.rule=Host(`${yaeger_rest_domain}`)
- traefik.http.routers.yaeger-rest.entrypoints=websecure
- traefik.http.routers.yaeger-rest.tls.certResolver=letsencrypt
- traefik.http.services.yaeger-rest.loadbalancer.server.port=1317
- traefik.http.routers.yaeger-grpc.rule=Host(`${yaeger_grpc_domain}`)
- traefik.http.routers.yaeger-grpc.entrypoints=websecure
- traefik.http.routers.yaeger-grpc.tls.certResolver=letsencrypt
- traefik.http.services.yaeger-grpc.loadbalancer.server.port=9090
- traefik.http.routers.yaeger-evm.rule=Host(`${yaeger_evm_domain}`)
- traefik.http.routers.yaeger-evm.entrypoints=websecure
- traefik.http.routers.yaeger-evm.tls.certResolver=letsencrypt
- traefik.http.services.yaeger-evm.loadbalancer.server.port=8545
depends_on:
- val-yaeger
restart: unless-stopped
ipfsctl:
image: ipfs/kubo:latest
container_name: ipfsctl
environment:
- IPFS_PROFILE=server
volumes:
- ipfs-data:/data/ipfs
networks:
- net-public
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.ipfs-api.rule=Host(`${ipfs_api_domain}`)
- traefik.http.routers.ipfs-api.entrypoints=websecure
- traefik.http.routers.ipfs-api.tls.certResolver=letsencrypt
- traefik.http.services.ipfs-api.loadbalancer.server.port=5001
- traefik.http.routers.ipfs-gateway.rule=Host(`${ipfs_gateway_domain}`)
- traefik.http.routers.ipfs-gateway.entrypoints=websecure
- traefik.http.routers.ipfs-gateway.tls.certResolver=letsencrypt
- traefik.http.services.ipfs-gateway.loadbalancer.server.port=8080
restart: unless-stopped
volumes:
ipfs-data:
+107
View File
@@ -0,0 +1,107 @@
[variables]
# Naruto Sentry Domains
naruto_rpc_domain = "${domain}"
naruto_rest_domain = "${domain}"
naruto_grpc_domain = "${domain}"
naruto_evm_domain = "${domain}"
# Senku Sentry Domains
senku_rpc_domain = "${domain}"
senku_rest_domain = "${domain}"
senku_grpc_domain = "${domain}"
senku_evm_domain = "${domain}"
# Yaeger Sentry Domains
yaeger_rpc_domain = "${domain}"
yaeger_rest_domain = "${domain}"
yaeger_grpc_domain = "${domain}"
yaeger_evm_domain = "${domain}"
# IPFS Domains
ipfs_api_domain = "${domain}"
ipfs_gateway_domain = "${domain}"
[config]
# Naruto Sentry RPC
[[config.domains]]
serviceName = "sentry-naruto"
port = 26657
host = "${naruto_rpc_domain}"
# Naruto Sentry REST
[[config.domains]]
serviceName = "sentry-naruto"
port = 1317
host = "${naruto_rest_domain}"
# Naruto Sentry gRPC
[[config.domains]]
serviceName = "sentry-naruto"
port = 9090
host = "${naruto_grpc_domain}"
# Naruto Sentry EVM
[[config.domains]]
serviceName = "sentry-naruto"
port = 8545
host = "${naruto_evm_domain}"
# Senku Sentry RPC
[[config.domains]]
serviceName = "sentry-senku"
port = 26657
host = "${senku_rpc_domain}"
# Senku Sentry REST
[[config.domains]]
serviceName = "sentry-senku"
port = 1317
host = "${senku_rest_domain}"
# Senku Sentry gRPC
[[config.domains]]
serviceName = "sentry-senku"
port = 9090
host = "${senku_grpc_domain}"
# Senku Sentry EVM
[[config.domains]]
serviceName = "sentry-senku"
port = 8545
host = "${senku_evm_domain}"
# Yaeger Sentry RPC
[[config.domains]]
serviceName = "sentry-yaeger"
port = 26657
host = "${yaeger_rpc_domain}"
# Yaeger Sentry REST
[[config.domains]]
serviceName = "sentry-yaeger"
port = 1317
host = "${yaeger_rest_domain}"
# Yaeger Sentry gRPC
[[config.domains]]
serviceName = "sentry-yaeger"
port = 9090
host = "${yaeger_grpc_domain}"
# Yaeger Sentry EVM
[[config.domains]]
serviceName = "sentry-yaeger"
port = 8545
host = "${yaeger_evm_domain}"
# IPFS API
[[config.domains]]
serviceName = "ipfsctl"
port = 5001
host = "${ipfs_api_domain}"
# IPFS Gateway
[[config.domains]]
serviceName = "ipfsctl"
port = 8080
host = "${ipfs_gateway_domain}"
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "snrd",
"name": "sonr",
"version": "0.1.0",
"description": "Plugin for Sonr PostgreSQL Docker image with pgsodium support and auto-start service",
"packages": ["docker@latest"],
@@ -19,7 +19,7 @@
"{{ .Virtenv }}/data": "",
"{{ .Virtenv }}/logs": "",
"{{ .Virtenv }}/process-compose.yaml": "etc/process-compose.yaml",
"{{ .DevboxDir }}/init.sh": "etc/init.sh"
"{{ .DevboxDir }}/init.sh": "scripts/test_node.sh"
},
"shell": {
"init_hook": ["chmod +x {{ .DevboxDir }}/init.sh", "{{ .DevboxDir }}/init.sh"],
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
set -euxo pipefail
mkdir -p /tmp/chains $UPGRADE_DIR
echo "Fetching code from tag"
mkdir -p /tmp/chains/$CHAIN_NAME
cd /tmp/chains/$CHAIN_NAME
if [[ $CODE_TAG =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Trying to fetch code from commit hash"
curl -LO $CODE_REPO/archive/$CODE_TAG.zip
unzip $CODE_TAG.zip
code_dir=${CODE_REPO##*/}-${CODE_TAG}
elif [[ $CODE_TAG = v* ]]; then
echo "Trying to fetch code from tag with 'v' prefix"
curl -LO $CODE_REPO/archive/refs/tags/$CODE_TAG.zip
unzip $CODE_TAG.zip
code_dir=${CODE_REPO##*/}-${CODE_TAG#"v"}
else
echo "Trying to fetch code from tag or branch"
if curl -fsLO $CODE_REPO/archive/refs/tags/$CODE_TAG.zip; then
unzip $CODE_TAG.zip
code_dir=${CODE_REPO##*/}-$CODE_TAG
elif curl -fsLO $CODE_REPO/archive/refs/heads/$CODE_TAG.zip; then
unzip $(echo $CODE_TAG | rev | cut -d "/" -f 1 | rev).zip
code_dir=${CODE_REPO##*/}-${CODE_TAG/\//-}
else
echo "Tag or branch '$CODE_TAG' not found"
exit 1
fi
fi
echo "Fetch wasmvm if needed"
cd /tmp/chains/$CHAIN_NAME/$code_dir
WASM_VERSION=$(cat go.mod | grep -oe "github.com/CosmWasm/wasmvm v[0-9.]*" | cut -d ' ' -f 2)
if [[ WASM_VERSION != "" ]]; then
mkdir -p /tmp/chains/libwasmvm_muslc
cd /tmp/chains/libwasmvm_muslc
curl -LO https://github.com/CosmWasm/wasmvm/releases/download/$WASM_VERSION/libwasmvm_muslc.x86_64.a
cp libwasmvm_muslc.x86_64.a /lib/libwasmvm_muslc.a
fi
echo "Build chain binary"
cd /tmp/chains/$CHAIN_NAME/$code_dir
CGO_ENABLED=1 BUILD_TAGS="muslc linkstatic" LINK_STATICALLY=true LEDGER_ENABLED=false make install
echo "Copy created binary to the upgrade directories"
if [[ $UPGRADE_NAME == "genesis" ]]; then
mkdir -p $UPGRADE_DIR/genesis/bin
cp $GOBIN/$CHAIN_BIN $UPGRADE_DIR/genesis/bin
else
mkdir -p $UPGRADE_DIR/upgrades/$UPGRADE_NAME/bin
cp $GOBIN/$CHAIN_BIN $UPGRADE_DIR/upgrades/$UPGRADE_NAME/bin
fi
echo "Cleanup"
rm -rf /tmp/chains/$CHAIN_NAME
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# chain-rpc-ready.sh - Check if a CometBFT or Tendermint RPC service is ready
# Usage: chain-rpc-ready.sh [RPC_URL]
set -euo pipefail
RPC_URL=${1:-"http://localhost:26657"}
echo 1>&2 "Checking if $RPC_URL is ready..."
# Check if the RPC URL is reachable,
json=$(curl -s --connect-timeout 2 "$RPC_URL/status")
# and the bootstrap block state has been validated,
if [ "$(echo "$json" | jq -r '.result.sync_info | (.earliest_block_height < .latest_block_height)')" != true ]; then
echo 1>&2 "$RPC_URL is not ready: bootstrap block state has not been validated"
exit 1
fi
# and the node is not catching up.
if [ "$(echo "$json" | jq -r .result.sync_info.catching_up)" != false ]; then
echo 1>&2 "$RPC_URL is not ready: node is catching up"
exit 1
fi
echo "$json" | jq -r .result
exit 0
+93 -125
View File
@@ -1,146 +1,114 @@
#!/bin/bash
set -eux
# scripts/create-genesis.sh - Create genesis file for Sonr network
# generate_vrf_key generates a VRF keypair and stores it securely
generate_vrf_key() {
local home_dir="$1"
set -euo pipefail
if [[ -z "${home_dir}" ]]; then
echo "Error: HOME_DIR parameter is required" >&2
return 1
fi
# Source helper libraries
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/env.sh"
source "${SCRIPT_DIR}/lib/keys.sh"
source "${SCRIPT_DIR}/lib/genesis.sh"
local genesis_file="${home_dir}/config/genesis.json"
if [[ ! -f "${genesis_file}" ]]; then
echo "Error: Genesis file not found at ${genesis_file}" >&2
return 1
fi
local chain_id
chain_id=$(jq -r '.chain_id' "${genesis_file}" 2>/dev/null)
if [[ -z "${chain_id}" || "${chain_id}" == "null" ]]; then
echo "Error: Failed to extract chain-id from genesis file" >&2
return 1
fi
echo "Generating VRF keypair for network: ${chain_id}"
local entropy_seed
entropy_seed=$(echo -n "${chain_id}" | sha256sum | cut -d' ' -f1)
local seed_part1="${entropy_seed}"
local seed_part2
seed_part2=$(echo -n "${entropy_seed}" | sha256sum | cut -d' ' -f1)
local vrf_key_hex="${seed_part1}${seed_part2}"
if [[ ${#vrf_key_hex} -ne 128 ]]; then
echo "Error: Generated VRF key has incorrect size: ${#vrf_key_hex}" >&2
return 1
fi
local vrf_key_path="${home_dir}/vrf_secret.key"
mkdir -p "${home_dir}"
echo -n "${vrf_key_hex}" | xxd -r -p > "${vrf_key_path}"
chmod 0600 "${vrf_key_path}"
local file_size
file_size=$(wc -c < "${vrf_key_path}")
if [[ ${file_size} -ne 64 ]]; then
echo "Error: VRF key file has incorrect size: ${file_size} bytes" >&2
rm -f "${vrf_key_path}"
return 1
fi
echo "✓ VRF keypair generated for network: ${chain_id}"
echo "✓ VRF secret key stored securely: ${vrf_key_path}"
return 0
}
DENOM="${DENOM:=usnr}"
# Match init-testnet.sh allocation: 100000000000000000000000000snr = 100000000000000000000000000000000usnr
COINS="${COINS:=100000000000000000000000000000000$DENOM,100000000000000000000000000snr}"
CHAIN_ID="${CHAIN_ID:=sonrtest_1-1}"
CHAIN_BIN="${CHAIN_BIN:=snrd}"
CHAIN_DIR="${CHAIN_DIR:=$HOME/.sonr}"
KEYS_CONFIG="${KEYS_CONFIG:=configs/keys.json}"
# Initialize environment
init_env
# Set defaults for genesis creation
FAUCET_ENABLED="${FAUCET_ENABLED:=true}"
NUM_VALIDATORS="${NUM_VALIDATORS:=1}"
NUM_RELAYERS="${NUM_RELAYERS:=0}"
# check if the binary has genesis subcommand or not, if not, set CHAIN_GENESIS_CMD to empty
CHAIN_GENESIS_CMD=$($CHAIN_BIN 2>&1 | grep -q "genesis-related subcommands" && echo "genesis" || echo "")
# Match init-testnet.sh allocation: 100000000000000000000000000snr = 100000000000000000000000000000000usnr
BASE_COINS="${BASE_COINS:=100000000000000000000000000000000${DENOM},100000000000000000000000000snr}"
VALIDATOR_AMOUNT="1000000000000000000000000000${DENOM}"
jq -r ".genesis[0].mnemonic" "$KEYS_CONFIG" | $CHAIN_BIN init "$CHAIN_ID" --chain-id "$CHAIN_ID" --default-denom "$DENOM" --recover
# Add genesis keys to the keyring and self delegate initial coins
echo "Adding key...." $(jq -r ".genesis[0].name" "$KEYS_CONFIG")
jq -r ".genesis[0].mnemonic" "$KEYS_CONFIG" | $CHAIN_BIN keys add $(jq -r ".genesis[0].name" "$KEYS_CONFIG") --recover --keyring-backend="test"
$CHAIN_BIN "$CHAIN_GENESIS_CMD" add-genesis-account $($CHAIN_BIN keys show -a $(jq -r .genesis[0].name "$KEYS_CONFIG") --keyring-backend="test") "$COINS" --keyring-backend="test"
# Add faucet key to the keyring and self delegate initial coins
echo "Adding key...." $(jq -r ".faucet[0].name" "$KEYS_CONFIG")
jq -r ".faucet[0].mnemonic" "$KEYS_CONFIG" | $CHAIN_BIN keys add $(jq -r ".faucet[0].name" "$KEYS_CONFIG") --recover --keyring-backend="test"
$CHAIN_BIN "$CHAIN_GENESIS_CMD" add-genesis-account $($CHAIN_BIN keys show -a $(jq -r .faucet[0].name "$KEYS_CONFIG") --keyring-backend="test") "$COINS" --keyring-backend="test"
# Add test keys to the keyring and self delegate initial coins
echo "Adding key...." $(jq -r ".keys[0].name" "$KEYS_CONFIG")
jq -r ".keys[0].mnemonic" "$KEYS_CONFIG" | $CHAIN_BIN keys add $(jq -r ".keys[0].name" "$KEYS_CONFIG") --recover --keyring-backend="test"
$CHAIN_BIN "$CHAIN_GENESIS_CMD" add-genesis-account $($CHAIN_BIN keys show -a $(jq -r .keys[0].name "$KEYS_CONFIG") --keyring-backend="test") "$COINS" --keyring-backend="test"
if [[ $FAUCET_ENABLED == "false" && $NUM_RELAYERS -gt "-1" ]]; then
## Add relayers keys and delegate tokens
for i in $(seq 0 "$NUM_RELAYERS"); do
# Add relayer key and delegate tokens
RELAYER_KEY_NAME="$(jq -r ".relayers[$i].name" "$KEYS_CONFIG")"
echo "Adding relayer key.... $RELAYER_KEY_NAME"
jq -r ".relayers[$i].mnemonic" "$KEYS_CONFIG" | $CHAIN_BIN keys add "$RELAYER_KEY_NAME" --recover --keyring-backend="test"
$CHAIN_BIN "$CHAIN_GENESIS_CMD" add-genesis-account $($CHAIN_BIN keys show -a "$RELAYER_KEY_NAME" --keyring-backend="test") "$COINS" --keyring-backend="test"
# Add relayer-cli key and delegate tokens
RELAYER_CLI_KEY_NAME="$(jq -r ".relayers_cli[$i].name" "$KEYS_CONFIG")"
echo "Adding relayer-cli key.... $RELAYER_CLI_KEY_NAME"
jq -r ".relayers_cli[$i].mnemonic" "$KEYS_CONFIG" | $CHAIN_BIN keys add "$RELAYER_CLI_KEY_NAME" --recover --keyring-backend="test"
$CHAIN_BIN "$CHAIN_GENESIS_CMD" add-genesis-account $($CHAIN_BIN keys show -a "$RELAYER_CLI_KEY_NAME" --keyring-backend="test") "$COINS" --keyring-backend="test"
done
# Check if binary has genesis subcommand
CHAIN_GENESIS_CMD=""
if $CHAIN_BIN 2>&1 | grep -q "genesis-related subcommands"; then
CHAIN_GENESIS_CMD="genesis"
fi
## if faucet not enabled then add validator and relayer with index as keys and into gentx
if [[ $FAUCET_ENABLED == "false" && $NUM_VALIDATORS -gt "1" ]]; then
## Add validators key and delegate tokens
for i in $(seq 0 "$NUM_VALIDATORS"); do
VAL_KEY_NAME="$(jq -r '.validators[0].name' "$KEYS_CONFIG")-$i"
echo "Adding validator key.... $VAL_KEY_NAME"
jq -r ".validators[0].mnemonic" "$KEYS_CONFIG" | $CHAIN_BIN keys add "$VAL_KEY_NAME" --index "$i" --recover --keyring-backend="test"
$CHAIN_BIN "$CHAIN_GENESIS_CMD" add-genesis-account $($CHAIN_BIN keys show -a "$VAL_KEY_NAME" --keyring-backend="test") "$COINS" --keyring-backend="test"
done
log_info "Creating genesis for Sonr network: $CHAIN_ID"
# Initialize chain
local genesis_mnemonic
genesis_mnemonic=$(jq -r ".genesis[0].mnemonic" "$KEYS_CONFIG")
echo "$genesis_mnemonic" | $CHAIN_BIN init "$CHAIN_ID" --chain-id "$CHAIN_ID" --default-denom "$DENOM" --recover
# Add genesis accounts
log_info "Adding genesis accounts..."
# Genesis key
local genesis_key_name
genesis_key_name=$(jq -r ".genesis[0].name" "$KEYS_CONFIG")
import_mnemonic "$genesis_key_name" "$genesis_mnemonic"
fund_key "$genesis_key_name" "$BASE_COINS"
# Faucet key
local faucet_key_name
faucet_key_name=$(jq -r ".faucet[0].name" "$KEYS_CONFIG")
local faucet_mnemonic
faucet_mnemonic=$(jq -r ".faucet[0].mnemonic" "$KEYS_CONFIG")
import_mnemonic "$faucet_key_name" "$faucet_mnemonic"
fund_key "$faucet_key_name" "$BASE_COINS"
# Test key
local test_key_name
test_key_name=$(jq -r ".keys[0].name" "$KEYS_CONFIG")
local test_mnemonic
test_mnemonic=$(jq -r ".keys[0].mnemonic" "$KEYS_CONFIG")
import_mnemonic "$test_key_name" "$test_mnemonic"
fund_key "$test_key_name" "$BASE_COINS"
# Add relayer keys if faucet is disabled
if [[ "$FAUCET_ENABLED" == "false" && "$NUM_RELAYERS" -gt 0 ]]; then
for i in $(seq 0 "$NUM_RELAYERS"); do
local relayer_key_name
relayer_key_name=$(jq -r ".relayers[$i].name" "$KEYS_CONFIG")
local relayer_mnemonic
relayer_mnemonic=$(jq -r ".relayers[$i].mnemonic" "$KEYS_CONFIG")
import_mnemonic "$relayer_key_name" "$relayer_mnemonic"
fund_key "$relayer_key_name" "$BASE_COINS"
local relayer_cli_key_name
relayer_cli_key_name=$(jq -r ".relayers_cli[$i].name" "$KEYS_CONFIG")
local relayer_cli_mnemonic
relayer_cli_mnemonic=$(jq -r ".relayers_cli[$i].mnemonic" "$KEYS_CONFIG")
import_mnemonic "$relayer_cli_key_name" "$relayer_cli_mnemonic"
fund_key "$relayer_cli_key_name" "$BASE_COINS"
done
fi
echo "Creating gentx..."
COIN=$(echo "$COINS" | cut -d ',' -f1)
# Use full validator amount to meet minimum delegation requirement (274890886240)
# Match the working init-testnet.sh: 1000000000000000000000snr = 1000000000000000000000000000usnr
VALIDATOR_AMOUNT="1000000000000000000000000000$DENOM"
$CHAIN_BIN "$CHAIN_GENESIS_CMD" gentx $(jq -r ".genesis[0].name" "$KEYS_CONFIG") "$VALIDATOR_AMOUNT" --keyring-backend="test" --chain-id "$CHAIN_ID" --gas-prices="0$DENOM"
# Add additional validator keys if needed
if [[ "$FAUCET_ENABLED" == "false" && "$NUM_VALIDATORS" -gt 1 ]]; then
for i in $(seq 1 "$NUM_VALIDATORS"); do
local val_key_name="${genesis_key_name}-${i}"
local val_mnemonic
val_mnemonic=$(jq -r ".validators[0].mnemonic" "$KEYS_CONFIG")
import_mnemonic "$val_key_name" "$val_mnemonic"
fund_key "$val_key_name" "$BASE_COINS"
done
fi
echo "Output of gentx"
cat "$CHAIN_DIR"/config/gentx/*.json | jq
# Create genesis transaction
log_info "Creating genesis transaction..."
$CHAIN_BIN "$CHAIN_GENESIS_CMD" gentx "$genesis_key_name" "$VALIDATOR_AMOUNT" \
--keyring-backend "$KEYRING_BACKEND" \
--chain-id "$CHAIN_ID" \
--gas-prices "0${DENOM}"
echo "Running collect-gentxs"
log_info "Genesis transaction output:"
cat "$CHAIN_DIR/config/gentx"/*.json | jq
# Collect genesis transactions
log_info "Collecting genesis transactions..."
$CHAIN_BIN "$CHAIN_GENESIS_CMD" collect-gentxs
ls "$CHAIN_DIR"/config
# Generate VRF keypair
echo ""
echo "Generating VRF keypair..."
if ! generate_vrf_key "${CHAIN_DIR}"; then
echo "Warning: VRF key generation failed, but continuing..."
echo "Note: Multi-validator encryption features may not work without VRF keys"
log_info "Generating VRF keypair..."
if ! generate_vrf_key "$CHAIN_DIR"; then
log_warn "VRF key generation failed, but continuing..."
log_warn "Note: Multi-validator encryption features may not work without VRF keys"
fi
log_success "Genesis creation completed"
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# scripts/create-ics.sh - Create ICS proposal for Sonr network
set -euo pipefail
# Source helper libraries
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/env.sh"
source "${SCRIPT_DIR}/lib/keys.sh"
source "${SCRIPT_DIR}/lib/tx.sh"
# Initialize environment
init_env
# Set defaults for ICS setup
KEY_NAME="${KEY_NAME:-ics-setup}"
STAKE_AMOUNT="10000000${DENOM}"
MAX_RETRIES="${MAX_RETRIES:-3}"
RETRY_INTERVAL="${RETRY_INTERVAL:-30}"
# Validate required parameters
if [[ -z "${PROPOSAL_FILE:-}" ]]; then
log_error "PROPOSAL_FILE environment variable is required"
exit 1
fi
ensure_file "$PROPOSAL_FILE"
log_info "Setting up ICS proposal with key '$KEY_NAME'"
# Import key from keys config if available
if [[ -f "${KEYS_CONFIG:-}" ]]; then
local key_mnemonic
key_mnemonic=$(jq -r ".keys[0].mnemonic" "$KEYS_CONFIG" 2>/dev/null || echo "")
if [[ -n "$key_mnemonic" && "$key_mnemonic" != "null" ]]; then
import_mnemonic "$KEY_NAME" "$key_mnemonic"
else
log_warn "No valid mnemonic found in $KEYS_CONFIG, using existing key or create one manually"
fi
else
log_warn "KEYS_CONFIG not set, ensure key '$KEY_NAME' exists"
fi
# Ensure key exists
ensure_key "$KEY_NAME"
# Get validator address and stake tokens
local validator_address
validator_address=$(get_validator_address)
stake_tokens "$KEY_NAME" "$validator_address" "$STAKE_AMOUNT"
# Determine proposal command (legacy vs new)
local submit_cmd="submit-proposal"
if $CHAIN_BIN tx gov --help 2>/dev/null | grep -q "submit-legacy-proposal"; then
submit_cmd="submit-legacy-proposal"
fi
log_info "Using proposal command: $submit_cmd"
# Submit proposal
local tx_hash
tx_hash=$(submit_proposal "$KEY_NAME" "$PROPOSAL_FILE" "consumer-addition")
# Extract proposal ID from transaction
local proposal_id
proposal_id=$(query_chain tx "$tx_hash" | jq -r '.logs[0].events[] | select(.type=="submit_proposal").attributes[] | select(.key=="proposal_id").value // empty')
if [[ -z "$proposal_id" || "$proposal_id" == "null" ]]; then
log_error "Failed to extract proposal ID from transaction"
exit 1
fi
log_info "Proposal ID: $proposal_id"
# Vote on proposal
vote_proposal "$KEY_NAME" "$proposal_id" "yes"
# Wait for proposal to pass
wait_for_proposal "$proposal_id" "$MAX_RETRIES" "$RETRY_INTERVAL"
log_success "ICS proposal setup completed successfully"
+80
View File
@@ -0,0 +1,80 @@
#!/bin/bash
# scripts/create-validator.sh - Create a validator for Sonr network
set -euo pipefail
# Source helper libraries
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/env.sh"
source "${SCRIPT_DIR}/lib/keys.sh"
source "${SCRIPT_DIR}/lib/tx.sh"
# Initialize environment
init_env
# Ensure chain directory exists
ensure_chain_dir
# Set defaults for validator creation
VAL_NAME="${VAL_NAME:-$CHAIN_ID-validator}"
VALIDATOR_AMOUNT="5000000000${DENOM}"
log_info "Creating validator '$VAL_NAME' for Sonr network"
# Wait for node to sync
wait_for_sync 10
# Get Cosmos SDK version to determine validator creation format
set +e
cosmos_sdk_version=$($CHAIN_BIN version --long | sed -n 's/cosmos_sdk_version: \(.*\)/\1/p')
set -e
log_info "Cosmos SDK version: $cosmos_sdk_version"
# Create validator based on SDK version
if [[ "$cosmos_sdk_version" > "v0.50.0" ]]; then
log_info "Using Cosmos SDK v0.50+ validator creation format"
# Create validator JSON for v0.50+
local validator_json
validator_json=$(cat <<EOF
{
"pubkey": "$($CHAIN_BIN tendermint show-validator $NODE_ARGS)",
"amount": "$VALIDATOR_AMOUNT",
"moniker": "$VAL_NAME",
"commission-rate": "0.1",
"commission-max-rate": "0.2",
"commission-max-change-rate": "0.01",
"min-self-delegation": "1000000"
}
EOF
)
echo "$validator_json" > /tmp/validator.json
# Submit validator creation transaction
submit_tx "$VAL_NAME" staking create-validator /tmp/validator.json
rm -f /tmp/validator.json
else
log_info "Using legacy validator creation format"
# Check if min-self-delegation parameter is supported
local args=""
if $CHAIN_BIN tx staking create-validator --help 2>/dev/null | grep -q "min-self-delegation"; then
args="--min-self-delegation=1000000"
fi
# Submit validator creation transaction with legacy format
submit_tx "$VAL_NAME" staking create-validator \
--pubkey="$($CHAIN_BIN tendermint show-validator $NODE_ARGS)" \
--moniker "$VAL_NAME" \
--amount "$VALIDATOR_AMOUNT" \
--commission-rate="0.10" \
--commission-max-rate="0.20" \
--commission-max-change-rate="0.01" \
$args
fi
log_success "Validator '$VAL_NAME' created successfully"
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
REGISTRY_URL="$1"
CHAIN_1="$2"
CHAIN_2="$3"
set -eux
function connection_id() {
CONNECTION_ID=$(curl -s $REGISTRY_URL/ibc/$CHAIN_1/$CHAIN_2 | jq -r ".chain_1.connection_id")
echo $CONNECTION_ID
}
echo "Try to get connection id, if failed, wait for 2 seconds and try again"
max_tries=20
while [[ max_tries -gt 0 ]]
do
id=$(connection_id)
if [[ -n "$id" ]]; then
echo "Found connection id: $id"
exit 0
fi
echo "Failed to get connection id. Sleeping for 2 secs. Tries left $max_tries"
((max_tries--))
sleep 10
done
+121
View File
@@ -0,0 +1,121 @@
# Sonr Script Library
This directory contains modular helper scripts for Sonr blockchain operations. These helpers provide reusable functions for common tasks like environment setup, configuration management, key handling, and transaction operations.
## Library Structure
### `env.sh` - Environment Management
Centralizes default environment variables and provides utility functions for logging, validation, and system checks.
**Key Functions:**
- `init_env()` - Initialize environment with required tools and cleanup
- `log_info()`, `log_warn()`, `log_error()`, `log_success()` - Colored logging
- `require_cmd()`, `ensure_file()`, `ensure_binary()` - Validation helpers
- `is_docker()` - Check if running in Docker container
**Environment Variables:**
- `DENOM=usnr` - Default denomination
- `CHAIN_BIN=snrd` - Binary name
- `CHAIN_DIR=~/.sonr` - Chain data directory
- `CHAIN_ID=sonrtest_1-1` - Default chain ID
### `jq_patch.sh` - JSON Operations
Provides safe JSON patching operations using `jq` with temporary files and validation.
**Key Functions:**
- `patch_json(file, jq_expr)` - Apply jq expression to file
- `set_json_string()`, `set_json_number()`, `set_json_bool()` - Type-safe setters
- `json_path_exists()`, `get_json_value()` - Query helpers
- `validate_json()` - JSON validation
### `config.sh` - Configuration Management
Handles TOML configuration files for Cosmos SDK nodes using `crudini` or `sed` fallbacks.
**Key Functions:**
- `configure_node()` - Complete node configuration with ports and settings
- `enable_rpc()`, `enable_rest()`, `enable_grpc()` - Enable services
- `set_consensus_timeouts()`, `set_min_gas_prices()` - Parameter setters
- `set_toml_value()` - Generic TOML value setter
### `keys.sh` - Key Management
Provides functions for importing, managing, and using cryptographic keys.
**Key Functions:**
- `import_mnemonic()` - Import key from mnemonic phrase
- `ensure_key()`, `get_key_address()` - Key validation and retrieval
- `fund_key()`, `delegate_to_validator()` - Account operations
- `wait_for_sync()` - Node synchronization waiting
### `tx.sh` - Transaction Operations
Handles blockchain transactions with error handling and retry logic.
**Key Functions:**
- `submit_tx()` - Submit transaction with gas and error handling
- `query_chain()` - Query blockchain state
- `submit_proposal()`, `vote_proposal()` - Governance operations
- `stake_tokens()`, `get_balance()` - Staking operations
### `genesis.sh` - Genesis File Operations
Specialized functions for genesis file creation and modification.
**Key Functions:**
- `generate_vrf_key()` - Generate VRF keypair for network
- `update_genesis_params()` - Apply Sonr-specific genesis parameters
- `add_constitution()` - Add constitution to governance
- `validate_genesis()` - Genesis file validation
## Usage Examples
### Basic Environment Setup
```bash
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/env.sh"
source "${SCRIPT_DIR}/lib/config.sh"
init_env
ensure_chain_dir
configure_node "$CHAIN_DIR" --rpc-port 26657 --rest-port 1317
```
### Key Management
```bash
#!/bin/bash
source "scripts/lib/keys.sh"
import_mnemonic "mykey" "word1 word2 word3..." "eth_secp256k1"
fund_key "mykey" "1000000usnr"
```
### Genesis Creation
```bash
#!/bin/bash
source "scripts/lib/genesis.sh"
update_genesis_params
add_constitution
generate_vrf_key "$CHAIN_DIR"
```
## Integration Guidelines
1. **Always source required libraries** at the top of scripts
2. **Call `init_env()`** early to set up logging and validation
3. **Use consistent error handling** with the provided logging functions
4. **Validate inputs** using `ensure_*` functions before operations
5. **Handle Docker vs local execution** in wrapper functions
6. **Use descriptive log messages** for user feedback
## Error Handling
All functions use consistent error handling:
- Functions return 0 on success, 1 on failure
- Use `log_error()` for user-visible errors
- Use `log_warn()` for non-fatal issues
- Cleanup temporary files automatically via `trap`
## Docker Compatibility
All functions support both local and Docker execution modes:
- Use `is_docker()` to detect environment
- Use `run_binary()` wrapper for command execution
- Mount volumes correctly for Docker containers
- Handle TTY and interactive mode appropriately
+341
View File
@@ -0,0 +1,341 @@
#!/bin/bash
# scripts/lib/config.sh - Configuration file utilities for TOML and other formats
set -euo pipefail
# Source environment and JSON helpers
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/jq_patch.sh"
# Check if crudini is available for TOML operations
CRUDINI_AVAILABLE=false
if command -v crudini >/dev/null 2>&1; then
CRUDINI_AVAILABLE=true
fi
# Set TOML value using crudini if available, fallback to sed
# Usage: set_toml_value <file> <section> <key> <value>
set_toml_value() {
local file="$1"
local section="$2"
local key="$3"
local value="$4"
ensure_file "$file"
if [[ "$CRUDINI_AVAILABLE" == "true" ]]; then
if crudini --set "$file" "$section" "$key" "$value" 2>/dev/null; then
log_success "Set $section.$key = $value in $file"
return 0
fi
fi
# Fallback to sed for simple cases
log_warn "Using sed fallback for TOML update (install crudini for better support)"
# Escape special characters in value
local escaped_value
escaped_value=$(printf '%s\n' "$value" | sed 's/[[\.*^$()+?{|]/\\&/g')
# Try to find and replace the line
if sed -i "s|^\s*$key\s*=.*|$key = \"$escaped_value\"|g" "$file"; then
log_success "Set $key = $value in $file (using sed)"
return 0
fi
log_error "Failed to set $section.$key in $file"
return 1
}
# Enable RPC server
# Usage: enable_rpc <config_file> [port]
enable_rpc() {
local config_file="$1"
local port="${2:-26657}"
ensure_file "$config_file"
log_info "Enabling RPC server on port $port"
# Update laddr
set_toml_value "$config_file" "" "laddr" "tcp://0.0.0.0:$port"
# Enable CORS
set_toml_value "$config_file" "" "cors_allowed_origins" '["*"]'
}
# Enable REST API server
# Usage: enable_rest <app_config_file> [port]
enable_rest() {
local app_config_file="$1"
local port="${2:-1317}"
ensure_file "$app_config_file"
log_info "Enabling REST API server on port $port"
# Update address
set_toml_value "$app_config_file" "api" "address" "tcp://0.0.0.0:$port"
# Enable API
set_toml_value "$app_config_file" "api" "enable" "true"
# Enable unsafe CORS
set_toml_value "$app_config_file" "api" "enabled-unsafe-cors" "true"
}
# Enable gRPC server
# Usage: enable_grpc <app_config_file> [port]
enable_grpc() {
local app_config_file="$1"
local port="${2:-9090}"
ensure_file "$app_config_file"
log_info "Enabling gRPC server on port $port"
# Update address
set_toml_value "$app_config_file" "grpc" "address" "0.0.0.0:$port"
}
# Enable gRPC-Web server
# Usage: enable_grpc_web <app_config_file> [port]
enable_grpc_web() {
local app_config_file="$1"
local port="${2:-9091}"
ensure_file "$app_config_file"
log_info "Enabling gRPC-Web server on port $port"
# Update address
set_toml_value "$app_config_file" "grpc-web" "address" "0.0.0.0:$port"
}
# Enable JSON-RPC
# Usage: enable_json_rpc <app_config_file> [port] [ws_port]
enable_json_rpc() {
local app_config_file="$1"
local port="${2:-8545}"
local ws_port="${3:-8546}"
ensure_file "$app_config_file"
log_info "Enabling JSON-RPC on port $port (WebSocket: $ws_port)"
# Enable JSON-RPC
set_toml_value "$app_config_file" "json-rpc" "enable" "true"
# Set address
set_toml_value "$app_config_file" "json-rpc" "address" "0.0.0.0:$port"
# Set WebSocket address
set_toml_value "$app_config_file" "json-rpc" "ws-address" "0.0.0.0:$ws_port"
# Enable APIs
set_toml_value "$app_config_file" "json-rpc" "api" "eth,txpool,personal,net,debug,web3"
}
# Enable Rosetta API
# Usage: enable_rosetta <app_config_file> [port]
enable_rosetta() {
local app_config_file="$1"
local port="${2:-8080}"
ensure_file "$app_config_file"
log_info "Enabling Rosetta API on port $port"
# Update address
set_toml_value "$app_config_file" "rosetta" "address" "0.0.0.0:$port"
}
# Set consensus timeouts
# Usage: set_consensus_timeouts <config_file> [propose] [prevote] [precommit] [commit]
set_consensus_timeouts() {
local config_file="$1"
local timeout_propose="${2:-5s}"
local timeout_prevote="${3:-1s}"
local timeout_precommit="${4:-1s}"
local timeout_commit="${5:-5s}"
ensure_file "$config_file"
log_info "Setting consensus timeouts: propose=$timeout_propose, prevote=$timeout_prevote, precommit=$timeout_precommit, commit=$timeout_commit"
set_toml_value "$config_file" "consensus" "timeout_propose" "$timeout_propose"
set_toml_value "$config_file" "consensus" "timeout_prevote" "$timeout_prevote"
set_toml_value "$config_file" "consensus" "timeout_precommit" "$timeout_precommit"
set_toml_value "$config_file" "consensus" "timeout_commit" "$timeout_commit"
}
# Set pruning strategy
# Usage: set_pruning <app_config_file> [strategy]
set_pruning() {
local app_config_file="$1"
local strategy="${2:-default}"
ensure_file "$app_config_file"
log_info "Setting pruning strategy to $strategy"
set_toml_value "$app_config_file" "pruning" "strategy" "$strategy"
}
# Set minimum gas prices
# Usage: set_min_gas_prices <app_config_file> [price]
set_min_gas_prices() {
local app_config_file="$1"
local price="${2:-0${DENOM}}"
ensure_file "$app_config_file"
log_info "Setting minimum gas prices to $price"
set_toml_value "$app_config_file" "minimum-gas-prices" "minimum-gas-prices" "$price"
}
# Enable metrics
# Usage: enable_metrics <config_file> [retention_time]
enable_metrics() {
local config_file="$1"
local retention_time="${2:-3600}"
ensure_file "$config_file"
log_info "Enabling metrics with retention time ${retention_time}s"
# Enable prometheus in config.toml
set_toml_value "$config_file" "instrumentation" "prometheus" "true"
# Set retention time in app.toml
if [[ -f "$config_file" ]]; then
set_toml_value "$config_file" "telemetry" "prometheus-retention-time" "$retention_time"
fi
}
# Set keyring backend
# Usage: set_keyring_backend <client_config_file> [backend]
set_keyring_backend() {
local client_config_file="$1"
local backend="${2:-test}"
ensure_file "$client_config_file"
log_info "Setting keyring backend to $backend"
set_toml_value "$client_config_file" "keyring-backend" "keyring-backend" "$backend"
}
# Set chain ID in client config
# Usage: set_client_chain_id <client_config_file> [chain_id]
set_client_chain_id() {
local client_config_file="$1"
local chain_id="${2:-$CHAIN_ID}"
ensure_file "$client_config_file"
log_info "Setting client chain ID to $chain_id"
set_toml_value "$client_config_file" "chain-id" "chain-id" "$chain_id"
}
# Set client output format
# Usage: set_client_output <client_config_file> [format]
set_client_output() {
local client_config_file="$1"
local format="${2:-json}"
ensure_file "$client_config_file"
log_info "Setting client output format to $format"
set_toml_value "$client_config_file" "output" "output" "$format"
}
# Configure full node settings
# Usage: configure_node <config_dir> [options...]
configure_node() {
local config_dir="$1"
shift
ensure_chain_dir
local config_toml="$config_dir/config/config.toml"
local app_toml="$config_dir/config/app.toml"
local client_toml="$config_dir/config/client.toml"
log_info "Configuring node in $config_dir"
# Apply configurations based on arguments
while [[ $# -gt 0 ]]; do
case $1 in
--rpc-port)
enable_rpc "$config_toml" "$2"
shift 2
;;
--rest-port)
enable_rest "$app_toml" "$2"
shift 2
;;
--grpc-port)
enable_grpc "$app_toml" "$2"
shift 2
;;
--grpc-web-port)
enable_grpc_web "$app_toml" "$2"
shift 2
;;
--json-rpc-port)
enable_json_rpc "$app_toml" "$2" "$3"
shift 3
;;
--rosetta-port)
enable_rosetta "$app_toml" "$2"
shift 2
;;
--consensus-timeouts)
set_consensus_timeouts "$config_toml" "$2" "$3" "$4" "$5"
shift 5
;;
--pruning)
set_pruning "$app_toml" "$2"
shift 2
;;
--min-gas-prices)
set_min_gas_prices "$app_toml" "$2"
shift 2
;;
--metrics)
enable_metrics "$config_toml" "$2"
shift 2
;;
--keyring-backend)
set_keyring_backend "$client_toml" "$2"
shift 2
;;
--chain-id)
set_client_chain_id "$client_toml" "$2"
shift 2
;;
--output-format)
set_client_output "$client_toml" "$2"
shift 2
;;
*)
log_error "Unknown configuration option: $1"
return 1
;;
esac
done
log_success "Node configuration completed"
}
# Export functions
export -f set_toml_value enable_rpc enable_rest enable_grpc enable_grpc_web
export -f enable_json_rpc enable_rosetta set_consensus_timeouts set_pruning
export -f set_min_gas_prices enable_metrics set_keyring_backend set_client_chain_id
export -f set_client_output configure_node
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# scripts/lib/env.sh - Environment defaults and utility functions for Sonr scripts
set -euo pipefail
# Default environment variables for Sonr
export DENOM="${DENOM:=usnr}"
export CHAIN_BIN="${CHAIN_BIN:=snrd}"
export CHAIN_DIR="${CHAIN_DIR:=$HOME/.sonr}"
export CHAIN_ID="${CHAIN_ID:=sonrtest_1-1}"
export KEYRING_BACKEND="${KEYRING_BACKEND:=test}"
export NODE_URL="${NODE_URL:=http://0.0.0.0:26657}"
export GAS="${GAS:=auto}"
export GAS_ADJUSTMENT="${GAS_ADJUSTMENT:=1.5}"
# Colors for logging
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $*"
}
# Utility functions
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
log_error "Required command '$cmd' not found. Please install it."
exit 1
fi
}
join_path() {
local base="$1"
local path="$2"
echo "$base/$path" | sed 's#//#/#g'
}
# Check if running in Docker
is_docker() {
[[ -f /.dockerenv ]] || [[ -n "${DOCKER_CONTAINER:-}" ]]
}
# Get absolute path
abs_path() {
local path="$1"
if [[ -d "$path" ]]; then
cd "$path" && pwd
else
cd "$(dirname "$path")" && echo "$(pwd)/$(basename "$path")"
fi
}
# Validate chain directory exists
ensure_chain_dir() {
if [[ ! -d "$CHAIN_DIR" ]]; then
log_error "Chain directory does not exist: $CHAIN_DIR"
exit 1
fi
}
# Validate binary exists
ensure_binary() {
if ! command -v "$CHAIN_BIN" >/dev/null 2>&1; then
log_error "Binary '$CHAIN_BIN' not found in PATH"
exit 1
fi
}
# Check if file exists
ensure_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
log_error "Required file not found: $file"
exit 1
fi
}
# Retry function for operations that might fail
retry() {
local max_attempts="$1"
local delay="$2"
local cmd="$3"
shift 3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
log_info "Attempt $attempt/$max_attempts: $cmd $*"
if "$cmd" "$@"; then
return 0
fi
if [[ $attempt -lt $max_attempts ]]; then
log_warn "Command failed, retrying in ${delay}s..."
sleep "$delay"
fi
((attempt++))
done
log_error "Command failed after $max_attempts attempts: $cmd $*"
return 1
}
# Initialize environment
init_env() {
require_cmd jq
ensure_binary
# Set up cleanup trap
trap cleanup EXIT
log_info "Environment initialized for Sonr chain: $CHAIN_ID"
}
cleanup() {
# Remove temporary files if they exist
rm -f /tmp/genesis.json /tmp/config.toml /tmp/app.toml /tmp/client.toml
}
# Export functions for use in other scripts
export -f log_info log_warn log_error log_success
export -f require_cmd join_path is_docker abs_path
export -f ensure_chain_dir ensure_binary ensure_file
export -f retry init_env cleanup
+210
View File
@@ -0,0 +1,210 @@
#!/bin/bash
# scripts/lib/genesis.sh - Genesis file utilities for Sonr scripts
set -euo pipefail
# Source environment and JSON helpers
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/jq_patch.sh"
# Generate VRF keypair for the network
# Usage: generate_vrf_key [chain_dir]
generate_vrf_key() {
local chain_dir="${1:-$CHAIN_DIR}"
ensure_chain_dir
local genesis_file="$chain_dir/config/genesis.json"
ensure_file "$genesis_file"
local chain_id
chain_id=$(get_json_value "$genesis_file" '.chain_id')
if [[ -z "$chain_id" || "$chain_id" == "null" ]]; then
log_error "Failed to extract chain-id from genesis file"
return 1
fi
log_info "Generating VRF keypair for network: $chain_id"
# Create deterministic entropy from chain-id using SHA256
local entropy_seed
entropy_seed=$(echo -n "$chain_id" | sha256sum | cut -d' ' -f1)
# Generate 64 bytes of deterministic randomness
local seed_part1="$entropy_seed"
local seed_part2
seed_part2=$(echo -n "$entropy_seed" | sha256sum | cut -d' ' -f1)
# Combine to create 64 bytes of hex data
local vrf_key_hex="${seed_part1}${seed_part2}"
# Ensure we have exactly 128 hex characters (64 bytes)
if [[ ${#vrf_key_hex} -ne 128 ]]; then
log_error "Generated VRF key has incorrect size: ${#vrf_key_hex}"
return 1
fi
# Path to store VRF secret key
local vrf_key_path="$chain_dir/vrf_secret.key"
# Ensure directory exists
mkdir -p "$chain_dir"
# Convert hex to binary and write to file
echo -n "$vrf_key_hex" | xxd -r -p > "$vrf_key_path"
# Set restrictive permissions (owner read/write only)
chmod 0600 "$vrf_key_path"
# Validate file was created with correct size (64 bytes)
local file_size
file_size=$(wc -c < "$vrf_key_path")
if [[ $file_size -ne 64 ]]; then
log_error "VRF key file has incorrect size: ${file_size} bytes"
rm -f "$vrf_key_path"
return 1
fi
log_success "VRF keypair generated for network: $chain_id"
log_success "VRF secret key stored securely: $vrf_key_path"
return 0
}
# Update genesis with Sonr-specific parameters
# Usage: update_genesis_params [genesis_file]
update_genesis_params() {
local genesis_file="${1:-$CHAIN_DIR/config/genesis.json}"
ensure_file "$genesis_file"
log_info "Updating genesis parameters for Sonr"
# Update stake denomination
set_json_string "$genesis_file" 'app_state.staking.params.bond_denom' "$DENOM"
# Update mint denomination
set_json_string "$genesis_file" 'app_state.mint.params.mint_denom' "$DENOM"
# Update crisis fee
set_json_object "$genesis_file" 'app_state.crisis.constant_fee' "{\"denom\":\"$DENOM\",\"amount\":\"1000\"}"
# Update minimum commission rate
set_json_string "$genesis_file" 'app_state.staking.params.min_commission_rate' "0.050000000000000000"
# Update block max gas
set_json_string "$genesis_file" 'consensus.params.block.max_gas' "100000000000"
# Update unbonding time
set_json_string "$genesis_file" 'app_state.staking.params.unbonding_time' "300s"
# Update downtime jail duration
set_json_string "$genesis_file" 'app_state.slashing.params.downtime_jail_duration' "60s"
# Update governance parameters for SDK v0.47+
if json_path_exists "$genesis_file" '.app_state.gov.params'; then
set_json_string "$genesis_file" 'app_state.gov.params.max_deposit_period' "30s"
set_json_string "$genesis_file" 'app_state.gov.params.min_deposit[0].amount' "10"
set_json_string "$genesis_file" 'app_state.gov.params.voting_period' "30s"
set_json_string "$genesis_file" 'app_state.gov.params.quorum' "0.000000000000000000"
set_json_string "$genesis_file" 'app_state.gov.params.threshold' "0.000000000000000000"
set_json_string "$genesis_file" 'app_state.gov.params.veto_threshold' "0.000000000000000000"
fi
# Update EVM parameters if present
if json_path_exists "$genesis_file" '.app_state.evm'; then
set_json_string "$genesis_file" 'app_state.evm.params.evm_denom' "$DENOM"
set_json_object "$genesis_file" 'app_state.evm.params.active_static_precompiles' '["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805"]'
fi
# Update ERC20 parameters if present
if json_path_exists "$genesis_file" '.app_state.erc20'; then
set_json_object "$genesis_file" 'app_state.erc20.params.native_precompiles' '["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]'
set_json_object "$genesis_file" 'app_state.erc20.token_pairs' '[{"contract_owner":1,"erc20_address":"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE","denom":"'"$DENOM"'","enabled":true}]'
fi
# Update feemarket parameters if present
if json_path_exists "$genesis_file" '.app_state.feemarket'; then
set_json_bool "$genesis_file" 'app_state.feemarket.params.no_base_fee' true
set_json_string "$genesis_file" 'app_state.feemarket.params.base_fee' "0.000000000000000000"
fi
# Update tokenfactory parameters if present
if json_path_exists "$genesis_file" '.app_state.tokenfactory'; then
set_json_object "$genesis_file" 'app_state.tokenfactory.params.denom_creation_fee' '[]'
set_json_number "$genesis_file" 'app_state.tokenfactory.params.denom_creation_gas_consume' 100000
fi
# Update ABCI parameters if present
if json_path_exists "$genesis_file" '.consensus.params.abci'; then
set_json_string "$genesis_file" 'consensus.params.abci.vote_extensions_enable_height' "1"
fi
log_success "Genesis parameters updated for Sonr"
}
# Add constitution to governance if CONSTITUTION.md exists
# Usage: add_constitution [genesis_file] [constitution_file]
add_constitution() {
local genesis_file="${1:-$CHAIN_DIR/config/genesis.json}"
local constitution_file="${2:-CONSTITUTION.md}"
ensure_file "$genesis_file"
# Look for CONSTITUTION.md in the git root directory
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local git_root
git_root="$(cd "${script_dir}/.." && pwd)"
local constitution_path="$git_root/$constitution_file"
if [[ ! -f "$constitution_path" ]]; then
log_warn "Constitution file not found: $constitution_path"
return 0
fi
log_info "Adding constitution from: $constitution_path"
local constitution_content
constitution_content=$(cat "$constitution_path" | jq -Rs .)
set_json_object "$genesis_file" 'app_state.gov.constitution' "$constitution_content"
log_success "Constitution added to governance"
}
# Validate genesis file
# Usage: validate_genesis [genesis_file]
validate_genesis() {
local genesis_file="${1:-$CHAIN_DIR/config/genesis.json}"
ensure_file "$genesis_file"
log_info "Validating genesis file..."
# Validate JSON
validate_json "$genesis_file"
# Check required fields
local chain_id
chain_id=$(get_json_value "$genesis_file" '.chain_id')
if [[ -z "$chain_id" || "$chain_id" == "null" ]]; then
log_error "Genesis file missing chain_id"
return 1
fi
# Check app_state exists
if ! json_path_exists "$genesis_file" '.app_state'; then
log_error "Genesis file missing app_state"
return 1
fi
log_success "Genesis file validation passed"
}
# Export functions
export -f generate_vrf_key update_genesis_params add_constitution validate_genesis
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
# scripts/lib/jq_patch.sh - JSON patching utilities for genesis and config files
set -euo pipefail
# Source environment helpers
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
# Patch JSON file with jq expression and save to temp file then move back
# Usage: patch_json <file> <jq_expression>
patch_json() {
local file="$1"
local jq_expr="$2"
ensure_file "$file"
log_info "Patching $file with: $jq_expr"
# Create temp file in same directory to avoid cross-filesystem issues
local temp_file
temp_file="$(dirname "$file")/.tmp.$(basename "$file").$$"
if ! jq -r "$jq_expr" "$file" > "$temp_file" 2>/dev/null; then
rm -f "$temp_file"
log_error "Failed to patch JSON file: $file"
return 1
fi
mv "$temp_file" "$file"
log_success "Successfully patched $file"
}
# Ensure array contains specific value
# Usage: ensure_array_contains <file> <path> <value>
ensure_array_contains() {
local file="$1"
local path="$2"
local value="$3"
ensure_file "$file"
local current_value
current_value=$(jq -r "$path // []" "$file")
# Check if value already exists
if [[ "$current_value" == *"\"$value\""* ]]; then
log_info "Value '$value' already exists in $path"
return 0
fi
# Add value to array
local jq_expr
jq_expr=".${path} += [\"$value\"]"
patch_json "$file" "$jq_expr"
}
# Set string value at path
# Usage: set_json_string <file> <path> <value>
set_json_string() {
local file="$1"
local path="$2"
local value="$3"
local jq_expr
jq_expr=".${path} = \"$value\""
patch_json "$file" "$jq_expr"
}
# Set numeric value at path
# Usage: set_json_number <file> <path> <value>
set_json_number() {
local file="$1"
local path="$2"
local value="$3"
local jq_expr
jq_expr=".${path} = $value"
patch_json "$file" "$jq_expr"
}
# Set boolean value at path
# Usage: set_json_bool <file> <path> <value>
set_json_bool() {
local file="$1"
local path="$2"
local value="$3"
local jq_expr
jq_expr=".${path} = $value"
patch_json "$file" "$jq_expr"
}
# Set object value at path
# Usage: set_json_object <file> <path> <json_object>
set_json_object() {
local file="$1"
local path="$2"
local json_object="$3"
local jq_expr
jq_expr=".${path} = $json_object"
patch_json "$file" "$jq_expr"
}
# Check if path exists and is not null
# Usage: json_path_exists <file> <path>
json_path_exists() {
local file="$1"
local path="$2"
ensure_file "$file"
local result
result=$(jq -r "$path // empty" "$file" 2>/dev/null)
[[ -n "$result" && "$result" != "null" ]]
}
# Get value at path
# Usage: get_json_value <file> <path>
get_json_value() {
local file="$1"
local path="$2"
ensure_file "$file"
jq -r "$path" "$file"
}
# Validate JSON file
# Usage: validate_json <file>
validate_json() {
local file="$1"
ensure_file "$file"
if ! jq empty "$file" >/dev/null 2>&1; then
log_error "Invalid JSON in file: $file"
return 1
fi
log_success "JSON validation passed for $file"
}
# Export functions
export -f patch_json ensure_array_contains set_json_string set_json_number
export -f set_json_bool set_json_object json_path_exists get_json_value validate_json
+211
View File
@@ -0,0 +1,211 @@
#!/bin/bash
# scripts/lib/keys.sh - Key management utilities for Sonr scripts
set -euo pipefail
# Source environment helpers
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
# Import mnemonic and add key to keyring
# Usage: import_mnemonic <key_name> <mnemonic> [algo]
import_mnemonic() {
local key_name="$1"
local mnemonic="$2"
local algo="${3:-eth_secp256k1}"
log_info "Importing key '$key_name'"
if [[ -z "$mnemonic" ]]; then
log_error "Mnemonic cannot be empty"
return 1
fi
# Use Docker wrapper if needed
if is_docker; then
echo "$mnemonic" | $CHAIN_BIN keys add "$key_name" \
--keyring-backend "$KEYRING_BACKEND" \
--algo "$algo" \
--recover
else
echo "$mnemonic" | $CHAIN_BIN keys add "$key_name" \
--keyring-backend "$KEYRING_BACKEND" \
--algo "$algo" \
--home "$CHAIN_DIR" \
--recover
fi
log_success "Key '$key_name' imported successfully"
}
# Ensure key exists in keyring
# Usage: ensure_key <key_name>
ensure_key() {
local key_name="$1"
if ! $CHAIN_BIN keys show "$key_name" \
--keyring-backend "$KEYRING_BACKEND" \
--home "$CHAIN_DIR" \
--output json >/dev/null 2>&1; then
log_error "Key '$key_name' not found in keyring"
return 1
fi
log_info "Key '$key_name' exists in keyring"
}
# Get key address
# Usage: get_key_address <key_name>
get_key_address() {
local key_name="$1"
ensure_key "$key_name"
local address
address=$($CHAIN_BIN keys show "$key_name" \
--keyring-backend "$KEYRING_BACKEND" \
--home "$CHAIN_DIR" \
--output json | jq -r '.address')
if [[ -z "$address" || "$address" == "null" ]]; then
log_error "Failed to get address for key '$key_name'"
return 1
fi
echo "$address"
}
# Fund key with tokens from genesis
# Usage: fund_key <key_name> <amount>
fund_key() {
local key_name="$1"
local amount="$2"
ensure_key "$key_name"
local address
address=$(get_key_address "$key_name")
log_info "Funding key '$key_name' ($address) with $amount"
if is_docker; then
$CHAIN_BIN genesis add-genesis-account "$address" "$amount" \
--keyring-backend "$KEYRING_BACKEND"
else
$CHAIN_BIN genesis add-genesis-account "$address" "$amount" \
--keyring-backend "$KEYRING_BACKEND" \
--home "$CHAIN_DIR"
fi
log_success "Funded key '$key_name' with $amount"
}
# Delegate tokens to validator
# Usage: delegate_to_validator <delegator_key> <validator_address> <amount>
delegate_to_validator() {
local delegator_key="$1"
local validator_address="$2"
local amount="$3"
ensure_key "$delegator_key"
log_info "Delegating $amount from '$delegator_key' to validator $validator_address"
$CHAIN_BIN tx staking delegate "$validator_address" "$amount" \
--from "$delegator_key" \
--chain-id "$CHAIN_ID" \
--node "$NODE_URL" \
--keyring-backend "$KEYRING_BACKEND" \
--gas "$GAS" \
--gas-adjustment "$GAS_ADJUSTMENT" \
--yes
log_success "Delegation completed"
}
# Create validator with key
# Usage: create_validator <key_name> <moniker> <amount> [options...]
create_validator() {
local key_name="$1"
local moniker="$2"
local amount="$3"
shift 3
ensure_key "$key_name"
local validator_address
validator_address=$(get_key_address "$key_name")
log_info "Creating validator '$moniker' with key '$key_name'"
# Build validator JSON
local validator_json
validator_json=$(cat <<EOF
{
"pubkey": "$($CHAIN_BIN tendermint show-validator)",
"amount": "$amount",
"moniker": "$moniker",
"commission-rate": "0.1",
"commission-max-rate": "0.2",
"commission-max-change-rate": "0.01",
"min-self-delegation": "1000000"
}
EOF
)
echo "$validator_json" > /tmp/validator.json
$CHAIN_BIN tx staking create-validator /tmp/validator.json \
--from "$key_name" \
--chain-id "$CHAIN_ID" \
--node "$NODE_URL" \
--keyring-backend "$KEYRING_BACKEND" \
--gas "$GAS" \
--gas-adjustment "$GAS_ADJUSTMENT" \
--yes
rm -f /tmp/validator.json
log_success "Validator '$moniker' created successfully"
}
# Wait for node to sync
# Usage: wait_for_sync [max_tries]
wait_for_sync() {
local max_tries="${1:-10}"
log_info "Waiting for node to sync..."
local tries=0
while [[ $tries -lt $max_tries ]]; do
if $CHAIN_BIN status \
--node "$NODE_URL" \
--output json 2>/dev/null | jq -e '.SyncInfo.catching_up == false' >/dev/null 2>&1; then
log_success "Node is synced"
return 0
fi
log_info "Still syncing... ($((tries + 1))/$max_tries)"
sleep 30
((tries++))
done
log_error "Node failed to sync after $max_tries attempts"
return 1
}
# List keys in keyring
# Usage: list_keys
list_keys() {
$CHAIN_BIN keys list \
--keyring-backend "$KEYRING_BACKEND" \
--home "$CHAIN_DIR" \
--output json | jq
}
# Export functions
export -f import_mnemonic ensure_key get_key_address fund_key
export -f delegate_to_validator create_validator wait_for_sync list_keys
+228
View File
@@ -0,0 +1,228 @@
#!/bin/bash
# scripts/lib/tx.sh - Transaction utilities for Sonr scripts
set -euo pipefail
# Source environment and key helpers
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/keys.sh"
# Submit transaction with automatic gas and error handling
# Usage: submit_tx <from_key> <command...>
submit_tx() {
local from_key="$1"
shift
ensure_key "$from_key"
log_info "Submitting transaction from '$from_key': $*"
local tx_result
tx_result=$($CHAIN_BIN tx "$@" \
--from "$from_key" \
--chain-id "$CHAIN_ID" \
--node "$NODE_URL" \
--keyring-backend "$KEYRING_BACKEND" \
--gas "$GAS" \
--gas-adjustment "$GAS_ADJUSTMENT" \
--output json \
--yes 2>&1)
# Check for transaction hash
local tx_hash
tx_hash=$(echo "$tx_result" | jq -r '.txhash // empty')
if [[ -n "$tx_hash" && "$tx_hash" != "null" ]]; then
log_success "Transaction submitted: $tx_hash"
# Wait for confirmation
sleep 5
# Query transaction result
local tx_query
tx_query=$($CHAIN_BIN query tx "$tx_hash" \
--node "$NODE_URL" \
--output json 2>/dev/null)
local tx_code
tx_code=$(echo "$tx_query" | jq -r '.code // 0')
if [[ "$tx_code" == "0" ]]; then
log_success "Transaction confirmed successfully"
echo "$tx_hash"
return 0
else
local tx_log
tx_log=$(echo "$tx_query" | jq -r '.raw_log // "Unknown error"')
log_error "Transaction failed (code $tx_code): $tx_log"
return 1
fi
else
log_error "Transaction submission failed: $tx_result"
return 1
fi
}
# Query with error handling
# Usage: query_chain <command...>
query_chain() {
log_info "Querying chain: $*"
local result
result=$($CHAIN_BIN query "$@" \
--node "$NODE_URL" \
--output json 2>/dev/null)
if [[ -z "$result" || "$result" == "null" ]]; then
log_error "Query failed or returned null"
return 1
fi
echo "$result"
}
# Get validator address (first available)
# Usage: get_validator_address
get_validator_address() {
log_info "Getting validator address..."
local validators
validators=$(query_chain staking validators)
local validator_address
validator_address=$(echo "$validators" | jq -r '.validators[0].operator_address // empty')
if [[ -z "$validator_address" || "$validator_address" == "null" ]]; then
log_error "No validators found"
return 1
fi
log_success "Using validator: $validator_address"
echo "$validator_address"
}
# Submit governance proposal
# Usage: submit_proposal <from_key> <proposal_file> [proposal_type]
submit_proposal() {
local from_key="$1"
local proposal_file="$2"
local proposal_type="${3:-}"
ensure_file "$proposal_file"
ensure_key "$from_key"
log_info "Submitting governance proposal from '$from_key'"
local submit_cmd="submit-proposal"
if [[ -n "$proposal_type" ]]; then
submit_cmd="$submit_cmd $proposal_type"
fi
# Check if legacy proposal command is needed
if ! $CHAIN_BIN tx gov submit-proposal --help 2>/dev/null | grep -q "submit-proposal"; then
submit_cmd="submit-legacy-proposal"
fi
submit_tx "$from_key" gov "$submit_cmd" "$proposal_file"
}
# Vote on governance proposal
# Usage: vote_proposal <from_key> <proposal_id> <vote_option>
vote_proposal() {
local from_key="$1"
local proposal_id="$2"
local vote_option="${3:-yes}"
ensure_key "$from_key"
log_info "Voting '$vote_option' on proposal $proposal_id from '$from_key'"
submit_tx "$from_key" gov vote "$proposal_id" "$vote_option"
}
# Wait for proposal to pass
# Usage: wait_for_proposal <proposal_id> [max_tries] [interval]
wait_for_proposal() {
local proposal_id="$1"
local max_tries="${2:-3}"
local interval="${3:-30}"
log_info "Waiting for proposal $proposal_id to pass..."
local tries=0
while [[ $tries -lt $max_tries ]]; do
local status
status=$(query_chain gov proposal "$proposal_id" | jq -r '.status // "unknown"')
case "$status" in
"PROPOSAL_STATUS_PASSED")
log_success "Proposal $proposal_id has passed"
return 0
;;
"PROPOSAL_STATUS_REJECTED")
log_error "Proposal $proposal_id was rejected"
return 1
;;
"PROPOSAL_STATUS_FAILED")
log_error "Proposal $proposal_id failed"
return 1
;;
*)
log_info "Proposal status: $status ($((tries + 1))/$max_tries)"
sleep "$interval"
((tries++))
;;
esac
done
log_error "Proposal $proposal_id did not pass after $max_tries attempts"
return 1
}
# Stake tokens to validator
# Usage: stake_tokens <from_key> <validator_address> <amount>
stake_tokens() {
local from_key="$1"
local validator_address="$2"
local amount="$3"
ensure_key "$from_key"
log_info "Staking $amount from '$from_key' to $validator_address"
submit_tx "$from_key" staking delegate "$validator_address" "$amount"
}
# Query account balance
# Usage: get_balance <address> [denom]
get_balance() {
local address="$1"
local denom="${2:-$DENOM}"
log_info "Getting balance for $address (denom: $denom)"
local balance
balance=$(query_chain bank balances "$address" | jq -r ".balances[] | select(.denom == \"$denom\") | .amount // \"0\"")
echo "$balance"
}
# Send tokens
# Usage: send_tokens <from_key> <to_address> <amount>
send_tokens() {
local from_key="$1"
local to_address="$2"
local amount="$3"
ensure_key "$from_key"
log_info "Sending $amount from '$from_key' to $to_address"
submit_tx "$from_key" bank send "$from_key" "$to_address" "$amount"
}
# Export functions
export -f submit_tx query_chain get_validator_address submit_proposal
export -f vote_proposal wait_for_proposal stake_tokens get_balance send_tokens
+123 -257
View File
@@ -5,156 +5,76 @@
# CHAIN_ID="localchain_9000-1" HOME_DIR="~/.sonr" BLOCK_TIME="1000ms" CLEAN=true sh scripts/test_node.sh
# CHAIN_ID="localchain_9000-2" HOME_DIR="~/.sonr" CLEAN=true RPC=36657 REST=2317 PROFF=6061 P2P=36656 GRPC=8090 GRPC_WEB=8091 ROSETTA=8081 BLOCK_TIME="500ms" sh scripts/test_node.sh
set -eu
set -euo pipefail
export KEY="acc0"
export KEY2="acc1"
# Source helper libraries
# Get the directory of this script reliably
SCRIPT_DIR="/usr/local/lib/sonr-scripts"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/config.sh"
source "${SCRIPT_DIR}/keys.sh"
source "${SCRIPT_DIR}/genesis.sh"
export CHAIN_ID=${CHAIN_ID:-"sonrtest_1-1"}
export MONIKER="localvalidator"
export KEYALGO="eth_secp256k1"
export KEYRING=${KEYRING:-"test"}
export HOME_DIR=$(eval echo "${HOME_DIR:-"~/.sonr"}")
export BINARY=${BINARY:-snrd}
export DENOM=${DENOM:-usnr}
# Initialize environment
init_env
export CLEAN=${CLEAN:-"false"}
export RPC=${RPC:-"26657"}
export REST=${REST:-"1317"}
export PROFF=${PROFF:-"6060"}
export P2P=${P2P:-"26656"}
export GRPC=${GRPC:-"9090"}
export GRPC_WEB=${GRPC_WEB:-"9091"}
export ROSETTA=${ROSETTA:-"8080"}
export JSON_RPC=${JSON_RPC:-"8545"}
export JSON_RPC_WS=${JSON_RPC_WS:-"8546"}
export BLOCK_TIME=${BLOCK_TIME:-"5s"}
# Set defaults for test node
export KEY="${KEY:-acc0}"
export KEY2="${KEY2:-acc1}"
export MONIKER="${MONIKER:-localvalidator}"
export KEYALGO="${KEYALGO:-eth_secp256k1}"
# Configurable Mnemomics
export SONR_MNEMONIC_1=${SONR_MNEMONIC_1:-"decorate bright ozone fork gallery riot bus exhaust worth way bone indoor calm squirrel merry zero scheme cotton until shop any excess stage laundry"}
export SONR_MNEMONIC_2=${SONR_MNEMONIC_2:-"wealth flavor believe regret funny network recall kiss grape useless pepper cram hint member few certain unveil rather brick bargain curious require crowd raise"}
# Configurable ports
export RPC="${RPC:-26657}"
export REST="${REST:-1317}"
export PROFF="${PROFF:-6060}"
export P2P="${P2P:-26656}"
export GRPC="${GRPC:-9090}"
export GRPC_WEB="${GRPC_WEB:-9091}"
export ROSETTA="${ROSETTA:-8080}"
export JSON_RPC="${JSON_RPC:-8545}"
export JSON_RPC_WS="${JSON_RPC_WS:-8546}"
export BLOCK_TIME="${BLOCK_TIME:-5s}"
# Configurable mnemonics
export SONR_MNEMONIC_1="${SONR_MNEMONIC_1:-decorate bright ozone fork gallery riot bus exhaust worth way bone indoor calm squirrel merry zero scheme cotton until shop any excess stage laundry}"
export SONR_MNEMONIC_2="${SONR_MNEMONIC_2:-wealth flavor believe regret funny network recall kiss grape useless pepper cram hint member few certain unveil rather brick bargain curious require crowd raise}"
# Docker and installation options
export FORCE_DOCKER="${FORCE_DOCKER:-false}"
export SKIP_INSTALL="${SKIP_INSTALL:-false}"
export CLEAN="${CLEAN:-false}"
export DOCKER_DETACHED="${DOCKER_DETACHED:-false}"
# Check if binary exists, if not use Docker (or force Docker if requested)
export FORCE_DOCKER=${FORCE_DOCKER:-"false"}
export SKIP_INSTALL=${SKIP_INSTALL:-"false"}
USE_DOCKER=false
if [[ "${FORCE_DOCKER}" == "true" ]] || [[ -z $(which "${BINARY}") ]]; then
if [[ "${FORCE_DOCKER}" == "true" ]] || ! command -v "$CHAIN_BIN" >/dev/null 2>&1; then
# Check if Docker is available and use it
if command -v docker >/dev/null 2>&1; then
if [[ "${FORCE_DOCKER}" == "true" ]]; then
echo "Force Docker mode enabled, using Docker image onsonr/snrd:latest..."
log_info "Force Docker mode enabled, using Docker image onsonr/snrd:latest"
else
echo "Binary ${BINARY} not found locally, checking for Docker image onsonr/snrd:latest..."
log_info "Binary $CHAIN_BIN not found locally, checking for Docker image onsonr/snrd:latest"
fi
if docker image inspect onsonr/snrd:latest >/dev/null 2>&1; then
echo "Using Docker image onsonr/snrd:latest"
log_info "Using Docker image onsonr/snrd:latest"
USE_DOCKER=true
else
echo "Docker image onsonr/snrd:latest not found. Pulling image..."
log_info "Docker image onsonr/snrd:latest not found. Pulling image..."
docker pull onsonr/snrd:latest || {
echo "Failed to pull onsonr/snrd:latest. Please ensure Docker is running and you have internet access."
log_error "Failed to pull onsonr/snrd:latest. Please ensure Docker is running and you have internet access."
exit 1
}
USE_DOCKER=true
fi
else
echo "Binary ${BINARY} not found. Please either:"
echo " 1. Install ${BINARY} with 'make install'"
echo " 2. Install Docker to use the containerized version"
log_error "Binary $CHAIN_BIN not found. Please either:"
log_error " 1. Install $CHAIN_BIN with 'make install'"
log_error " 2. Install Docker to use the containerized version"
exit 1
fi
fi
# Final check if not using Docker
if [[ "${USE_DOCKER}" == "false" ]]; then
command -v "${BINARY}" >/dev/null 2>&1 || {
echo >&2 "${BINARY} command not found. Ensure this is setup / properly installed in your GOPATH (make install)."
exit 1
}
fi
command -v jq >/dev/null 2>&1 || {
echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/"
exit 1
}
# generate_vrf_key generates a VRF keypair and stores it securely
# Mirrors the Go implementation in app/commands/enhance_init.go
generate_vrf_key() {
local home_dir="$1"
local use_docker="${2:-false}"
# Validate parameters
if [[ -z "${home_dir}" ]]; then
echo "Error: HOME_DIR parameter is required" >&2
return 1
fi
# Path to genesis file
local genesis_file="${home_dir}/config/genesis.json"
# Check if genesis file exists
if [[ ! -f "${genesis_file}" ]]; then
echo "Error: Genesis file not found at ${genesis_file}" >&2
return 1
fi
# Extract chain-id from genesis file
local chain_id
chain_id=$(jq -r '.chain_id' "${genesis_file}" 2>/dev/null)
if [[ -z "${chain_id}" || "${chain_id}" == "null" ]]; then
echo "Error: Failed to extract chain-id from genesis file" >&2
return 1
fi
echo "Generating VRF keypair for network: ${chain_id}"
# Create deterministic entropy from chain-id using SHA256
local entropy_seed
entropy_seed=$(echo -n "${chain_id}" | sha256sum | cut -d' ' -f1)
# Generate 64 bytes of deterministic randomness
local seed_part1="${entropy_seed}"
local seed_part2
seed_part2=$(echo -n "${entropy_seed}" | sha256sum | cut -d' ' -f1)
# Combine to create 64 bytes of hex data
local vrf_key_hex="${seed_part1}${seed_part2}"
# Ensure we have exactly 128 hex characters (64 bytes)
if [[ ${#vrf_key_hex} -ne 128 ]]; then
echo "Error: Generated VRF key has incorrect size: ${#vrf_key_hex}" >&2
return 1
fi
# Path to store VRF secret key
local vrf_key_path="${home_dir}/vrf_secret.key"
# Ensure directory exists
mkdir -p "${home_dir}"
# Convert hex to binary and write to file
echo -n "${vrf_key_hex}" | xxd -r -p > "${vrf_key_path}"
# Set restrictive permissions (owner read/write only)
chmod 0600 "${vrf_key_path}"
# Validate file was created with correct size (64 bytes)
local file_size
file_size=$(wc -c < "${vrf_key_path}")
if [[ ${file_size} -ne 64 ]]; then
echo "Error: VRF key file has incorrect size: ${file_size} bytes" >&2
rm -f "${vrf_key_path}"
return 1
fi
echo "✓ VRF keypair generated for network: ${chain_id}"
echo "✓ VRF secret key stored securely: ${vrf_key_path}"
return 0
}
# Create wrapper function for binary execution
run_binary() {
if [[ "${USE_DOCKER}" == "true" ]]; then
@@ -172,10 +92,11 @@ run_binary() {
onsonr/snrd:latest \
snrd --home /root/.sonr "$@"
else
${BINARY} "$@"
${CHAIN_BIN} "$@"
fi
}
# Set client configuration
set_config() {
run_binary config set client chain-id "${CHAIN_ID}"
run_binary config set client keyring-backend "${KEYRING}"
@@ -185,100 +106,49 @@ set_config
from_scratch() {
# Fresh install on current branch (skip if using Docker or SKIP_INSTALL is true)
if [[ "${USE_DOCKER}" == "false" ]] && [[ "${SKIP_INSTALL}" == "false" ]]; then
log_info "Installing $CHAIN_BIN..."
make install
fi
# remove existing daemon files.
# Remove existing daemon files
if [[ ${#HOME_DIR} -le 2 ]]; then
echo "HOME_DIR must be more than 2 characters long"
return
log_error "HOME_DIR must be more than 2 characters long"
return 1
fi
rm -rf "${HOME_DIR}" && echo "Removed ${HOME_DIR}"
rm -rf "${HOME_DIR}"
log_info "Removed existing chain directory: ${HOME_DIR}"
# reset values if not set already after whipe
# Reset configuration
set_config
add_key() {
key=$1
mnemonic=$2
if [[ "${USE_DOCKER}" == "true" ]]; then
# For Docker, we need to pass the mnemonic differently
mkdir -p "${HOME_DIR}"
echo "${mnemonic}" | docker run --rm -i \
-v "${HOME_DIR}:/root/.sonr" \
--network host \
onsonr/snrd:latest \
snrd --home /root/.sonr keys add "${key}" --keyring-backend "${KEYRING}" --algo "${KEYALGO}" --recover
else
echo "${mnemonic}" | ${BINARY} keys add "${key}" --keyring-backend "${KEYRING}" --algo "${KEYALGO}" --home "${HOME_DIR}" --recover
fi
}
# idx140fehngcrxvhdt84x729p3f0qmkmea8n570lrg
add_key "${KEY}" "${SONR_MNEMONIC_1}"
# idx1r6yue0vuyj9m7xw78npspt9drq2tmtvgcrf7sr
add_key "${KEY2}" "${SONR_MNEMONIC_2}"
# Add test keys
log_info "Adding test keys..."
import_mnemonic "${KEY}" "${SONR_MNEMONIC_1}" "$KEYALGO"
import_mnemonic "${KEY2}" "${SONR_MNEMONIC_2}" "$KEYALGO"
# Initialize chain
log_info "Initializing chain with moniker: $MONIKER"
if [[ "${USE_DOCKER}" == "true" ]]; then
# For Docker init, we need to handle it specially
docker run --rm \
-v "${HOME_DIR}:/root/.sonr" \
--network host \
onsonr/snrd:latest \
snrd --home /root/.sonr init "${MONIKER}" --chain-id "${CHAIN_ID}" --default-denom "${DENOM}"
else
${BINARY} init "${MONIKER}" --chain-id "${CHAIN_ID}" --default-denom "${DENOM}" --home "${HOME_DIR}"
${CHAIN_BIN} init "${MONIKER}" --chain-id "${CHAIN_ID}" --default-denom "${DENOM}" --home "${HOME_DIR}"
fi
update_test_genesis() {
cat "${HOME_DIR}"/config/genesis.json | jq "$1" >"${HOME_DIR}"/config/tmp_genesis.json && mv "${HOME_DIR}"/config/tmp_genesis.json "${HOME_DIR}"/config/genesis.json
}
# Update genesis parameters
log_info "Updating genesis parameters..."
update_genesis_params
# === CORE MODULES ===
# Add constitution if available
add_constitution
# Block
update_test_genesis '.consensus_params["block"]["max_gas"]="100000000"'
# Set up genesis accounts and transactions
local BASE_GENESIS_ALLOCATIONS="100000000000000000000000000${DENOM},100000000test"
# Gov
update_test_genesis $(printf '.app_state["gov"]["params"]["min_deposit"]=[{"denom":"%s","amount":"1000000"}]' "${DENOM}")
update_test_genesis '.app_state["gov"]["params"]["voting_period"]="30s"'
update_test_genesis '.app_state["gov"]["params"]["expedited_voting_period"]="15s"'
# Add CONSTITUTION.md to governance if it exists
if [ -f "CONSTITUTION.md" ]; then
CONSTITUTION_CONTENT=$(cat CONSTITUTION.md | jq -Rs .)
update_test_genesis ".app_state[\"gov\"][\"constitution\"]=$CONSTITUTION_CONTENT"
fi
update_test_genesis $(printf '.app_state["evm"]["params"]["evm_denom"]="%s"' "${DENOM}")
update_test_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805"]'
update_test_genesis '.app_state["erc20"]["params"]["native_precompiles"]=["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' # https://eips.ethereum.org/EIPS/eip-7528
update_test_genesis $(printf '.app_state["erc20"]["token_pairs"]=[{contract_owner:1,erc20_address:"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",denom:"%s",enabled:true}]' "${DENOM}")
update_test_genesis '.app_state["feemarket"]["params"]["no_base_fee"]=true'
update_test_genesis '.app_state["feemarket"]["params"]["base_fee"]="0.000000000000000000"'
# staking
update_test_genesis $(printf '.app_state["staking"]["params"]["bond_denom"]="%s"' "${DENOM}")
update_test_genesis '.app_state["staking"]["params"]["min_commission_rate"]="0.050000000000000000"'
# mint
update_test_genesis $(printf '.app_state["mint"]["params"]["mint_denom"]="%s"' "${DENOM}")
# crisis
update_test_genesis $(printf '.app_state["crisis"]["constant_fee"]={"denom":"%s","amount":"1000"}' "${DENOM}")
## abci
update_test_genesis '.consensus["params"]["abci"]["vote_extensions_enable_height"]="1"'
# === CUSTOM MODULES ===
# tokenfactory
update_test_genesis '.app_state["tokenfactory"]["params"]["denom_creation_fee"]=[]'
update_test_genesis '.app_state["tokenfactory"]["params"]["denom_creation_gas_consume"]=100000'
BASE_GENESIS_ALLOCATIONS="100000000000000000000000000${DENOM},100000000test"
# Allocate genesis accounts
log_info "Adding genesis accounts..."
if [[ "${USE_DOCKER}" == "true" ]]; then
docker run --rm \
-v "${HOME_DIR}:/root/.sonr" \
@@ -307,85 +177,78 @@ from_scratch() {
onsonr/snrd:latest \
snrd --home /root/.sonr genesis validate-genesis
else
${BINARY} genesis add-genesis-account "${KEY}" "${BASE_GENESIS_ALLOCATIONS}" --keyring-backend "${KEYRING}" --home "${HOME_DIR}" --append
${BINARY} genesis add-genesis-account "${KEY2}" "${BASE_GENESIS_ALLOCATIONS}" --keyring-backend "${KEYRING}" --home "${HOME_DIR}" --append
${CHAIN_BIN} genesis add-genesis-account "${KEY}" "${BASE_GENESIS_ALLOCATIONS}" --keyring-backend "${KEYRING}" --home "${HOME_DIR}" --append
${CHAIN_BIN} genesis add-genesis-account "${KEY2}" "${BASE_GENESIS_ALLOCATIONS}" --keyring-backend "${KEYRING}" --home "${HOME_DIR}" --append
# Sign genesis transaction
${BINARY} genesis gentx "${KEY}" 1000000000000000000000"${DENOM}" --gas-prices 0"${DENOM}" --keyring-backend "${KEYRING}" --chain-id "${CHAIN_ID}" --home "${HOME_DIR}"
${BINARY} genesis collect-gentxs --home "${HOME_DIR}"
${BINARY} genesis validate-genesis --home "${HOME_DIR}"
fi
err=$?
if [[ ${err} -ne 0 ]]; then
echo "Failed to validate genesis"
return
${CHAIN_BIN} genesis gentx "${KEY}" 1000000000000000000000"${DENOM}" --gas-prices 0"${DENOM}" --keyring-backend "${KEYRING}" --chain-id "${CHAIN_ID}" --home "${HOME_DIR}"
${CHAIN_BIN} genesis collect-gentxs --home "${HOME_DIR}"
${CHAIN_BIN} genesis validate-genesis --home "${HOME_DIR}"
fi
log_success "Genesis setup completed"
}
# check if CLEAN is not set to false
# Check if CLEAN is not set to false
if [[ ${CLEAN} != "false" ]]; then
echo "Starting from a clean state"
log_info "Starting from a clean state"
from_scratch
# Generate VRF keypair (must be done after genesis file is created)
echo ""
echo "Generating VRF keypair..."
if ! generate_vrf_key "${HOME_DIR}" "${USE_DOCKER}"; then
echo "Warning: VRF key generation failed, but continuing..."
echo "Note: Multi-validator encryption features may not work without VRF keys"
log_info "Generating VRF keypair..."
if ! generate_vrf_key "${HOME_DIR}"; then
log_warn "VRF key generation failed, but continuing..."
log_warn "Note: Multi-validator encryption features may not work without VRF keys"
fi
fi
echo "Starting node..."
log_info "Configuring node ports and settings..."
# Opens the RPC endpoint to outside connections
sed -i -e 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:'"${RPC}"'"/g' "${HOME_DIR}"/config/config.toml
sed -i -e 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/g' "${HOME_DIR}"/config/config.toml
# Configure node with all the specified ports and settings
configure_node "$HOME_DIR" \
--rpc-port "$RPC" \
--rest-port "$REST" \
--grpc-port "$GRPC" \
--grpc-web-port "$GRPC_WEB" \
--json-rpc-port "$JSON_RPC" \
--rosetta-port "$ROSETTA" \
--min-gas-prices "0${DENOM}" \
--pruning nothing
# REST endpoint
sed -i -e 's/address = "tcp:\/\/localhost:1317"/address = "tcp:\/\/0.0.0.0:'"${REST}"'"/g' "${HOME_DIR}"/config/app.toml
sed -i -e 's/enable = false/enable = true/g' "${HOME_DIR}"/config/app.toml
sed -i -e 's/enabled-unsafe-cors = false/enabled-unsafe-cors = true/g' "${HOME_DIR}"/config/app.toml
# Set consensus timeouts
set_consensus_timeouts "$HOME_DIR/config/config.toml" "5s" "1s" "1s" "$BLOCK_TIME"
# peer exchange
sed -i -e 's/pprof_laddr = "localhost:6060"/pprof_laddr = "localhost:'"${PROFF}"'"/g' "${HOME_DIR}"/config/config.toml
sed -i -e 's/laddr = "tcp:\/\/0.0.0.0:26656"/laddr = "tcp:\/\/0.0.0.0:'"${P2P}"'"/g' "${HOME_DIR}"/config/config.toml
# Enable CORS for RPC
set_toml_value "$HOME_DIR/config/config.toml" "" "cors_allowed_origins" '["*"]'
# GRPC
sed -i -e 's/address = "localhost:9090"/address = "0.0.0.0:'"${GRPC}"'"/g' "${HOME_DIR}"/config/app.toml
sed -i -e 's/address = "localhost:9091"/address = "0.0.0.0:'"${GRPC_WEB}"'"/g' "${HOME_DIR}"/config/app.toml
# Set pprof address
set_toml_value "$HOME_DIR/config/config.toml" "" "pprof_laddr" "localhost:${PROFF}"
# Rosetta Api
sed -i -e 's/address = ":8080"/address = "0.0.0.0:'"${ROSETTA}"'"/g' "${HOME_DIR}"/config/app.toml
# Set P2P address
set_toml_value "$HOME_DIR/config/config.toml" "p2p" "laddr" "tcp://0.0.0.0:${P2P}"
# JSON-RPC
sed -i -e '/\[json-rpc\]/,/^\[/ s/enable = false/enable = true/' "${HOME_DIR}"/config/app.toml
sed -i -e '/\[json-rpc\]/,/^\[/ s/address = "127.0.0.1:8545"/address = "0.0.0.0:'"${JSON_RPC}"'"/' "${HOME_DIR}"/config/app.toml
sed -i -e '/\[json-rpc\]/,/^\[/ s/ws-address = "127.0.0.1:8546"/ws-address = "0.0.0.0:'"${JSON_RPC_WS}"'"/' "${HOME_DIR}"/config/app.toml
# Faster blocks
sed -i -e 's/timeout_commit = "5s"/timeout_commit = "'"${BLOCK_TIME}"'"/g' "${HOME_DIR}"/config/config.toml
log_info "Starting node..."
# Start the node (with or without Docker)
if [[ "${USE_DOCKER}" == "true" ]]; then
echo "Starting node using Docker..."
log_info "Starting node using Docker..."
# Check for detached mode via environment variable or prompt
DETACHED_MODE=""
if [[ "${DOCKER_DETACHED}" == "true" ]]; then
DETACHED_MODE="-d"
echo "Running in detached mode. Use 'docker logs -f sonr-testnode' to view logs."
echo "Stop with: docker stop sonr-testnode"
log_info "Running in detached mode. Use 'docker logs -f sonr-testnode' to view logs."
log_info "Stop with: docker stop sonr-testnode"
elif [ -t 0 ]; then
echo ""
echo "Would you like to run the node in detached mode (background)? [y/N]"
log_info ""
log_info "Would you like to run the node in detached mode (background)? [y/N]"
read -r -n 1 DETACH_RESPONSE
echo ""
log_info ""
if [[ "$DETACH_RESPONSE" =~ ^[Yy]$ ]]; then
DETACHED_MODE="-d"
echo "Running in detached mode. Use 'docker logs -f sonr-testnode' to view logs."
echo "Stop with: docker stop sonr-testnode"
log_info "Running in detached mode. Use 'docker logs -f sonr-testnode' to view logs."
log_info "Stop with: docker stop sonr-testnode"
else
echo "Running in foreground mode. Use Ctrl+C to stop."
log_info "Running in foreground mode. Use Ctrl+C to stop."
fi
fi
@@ -404,15 +267,18 @@ if [[ "${USE_DOCKER}" == "true" ]]; then
# If running detached, show status
if [ -n "$DETACHED_MODE" ]; then
echo ""
echo "Node started in background"
echo ""
echo "Useful commands:"
echo " View logs: docker logs -f sonr-testnode"
echo " Stop node: docker stop sonr-testnode"
echo " Node status: curl http://localhost:${RPC}/status | jq '.result.sync_info'"
echo ""
log_info ""
log_success "Node started in background"
log_info ""
log_info "Useful commands:"
log_info " View logs: docker logs -f sonr-testnode"
log_info " Stop node: docker stop sonr-testnode"
log_info " Node status: curl http://localhost:${RPC}/status | jq '.result.sync_info'"
log_info ""
fi
else
${BINARY} start --pruning=nothing --minimum-gas-prices=0"${DENOM}" --rpc.laddr="tcp://0.0.0.0:${RPC}" --home "${HOME_DIR}" --json-rpc.api=eth,txpool,personal,net,debug,web3 --json-rpc.address="0.0.0.0:${JSON_RPC}" --json-rpc.ws-address="0.0.0.0:${JSON_RPC_WS}" --chain-id="${CHAIN_ID}"
log_info "Starting node locally..."
${CHAIN_BIN} start --pruning=nothing --minimum-gas-prices=0"${DENOM}" --rpc.laddr="tcp://0.0.0.0:${RPC}" --home "${HOME_DIR}" --json-rpc.api=eth,txpool,personal,net,debug,web3 --json-rpc.address="0.0.0.0:${JSON_RPC}" --json-rpc.ws-address="0.0.0.0:${JSON_RPC_WS}" --chain-id="${CHAIN_ID}"
fi
log_success "Node startup completed"
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# scripts/testnet-setup.sh - Initialize Sonr testnet nodes for Docker deployment
set -euo pipefail
# Source helper libraries
SCRIPT_DIR="/usr/local/lib/sonr-scripts"
source "${SCRIPT_DIR}/env.sh"
source "${SCRIPT_DIR}/config.sh"
source "${SCRIPT_DIR}/keys.sh"
source "${SCRIPT_DIR}/genesis.sh"
# Initialize environment
init_env
# Set defaults for testnet
NODE_TYPE="${NODE_TYPE:-validator}" # validator, sentry
MONIKER="${MONIKER:-validator}"
VALIDATOR_NAME="${VALIDATOR_NAME:-$MONIKER}"
CHAIN_ID="${CHAIN_ID:-sonrtest_1-1}"
HOME_DIR="${HOME_DIR:-/root/.sonr}"
log_info "Setting up Sonr testnet node: $NODE_TYPE ($VALIDATOR_NAME)"
# Ensure chain directory exists
ensure_chain_dir
# Update genesis parameters
log_info "Updating genesis parameters..."
update_genesis_params
# Add constitution if available
add_constitution
# Configure node
log_info "Configuring node..."
configure_node "$CHAIN_DIR" \
--rpc-port 26657 \
--rest-port 1317 \
--grpc-port 9090 \
--grpc-web-port 9091 \
--json-rpc-port 8545 8546 \
--rosetta-port 8080 \
--min-gas-prices "0${DENOM}" \
--pruning nothing \
--keyring-backend "$KEYRING_BACKEND" \
--chain-id "$CHAIN_ID" \
--output-format json
# Set consensus timeouts for faster blocks
set_consensus_timeouts "$CHAIN_DIR/config/config.toml" "5s" "1s" "1s" "1s"
# Enable CORS for RPC
set_toml_value "$CHAIN_DIR/config/config.toml" "" "cors_allowed_origins" '["*"]'
# Set pprof address
set_toml_value "$CHAIN_DIR/config/config.toml" "" "pprof_laddr" "localhost:6060"
# Set P2P address
set_toml_value "$CHAIN_DIR/config/config.toml" "p2p" "laddr" "tcp://0.0.0.0:26656"
# Generate VRF keypair
log_info "Generating VRF keypair..."
if ! generate_vrf_key "$CHAIN_DIR"; then
log_warn "VRF key generation failed, but continuing..."
log_warn "Note: Multi-validator encryption features may not work without VRF keys"
fi
# Create validator if this is a validator node
if [[ "$NODE_TYPE" == "validator" ]]; then
log_info "Creating validator..."
# Wait for node to sync (in case it's connecting to other nodes)
if [[ "${WAIT_FOR_SYNC:-false}" == "true" ]]; then
wait_for_sync 10
fi
# Create validator (this would need proper key setup)
log_info "Validator creation requires manual key setup and gentx creation"
log_info "Run: snrd genesis gentx <validator-key> <amount> --chain-id $CHAIN_ID"
fi
log_success "Testnet setup completed for $NODE_TYPE node: $VALIDATOR_NAME"
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
ADDRESS="$1"
DENOM="$2"
FAUCET_URL="$3"
FAUCET_ENABLED="$4"
set -eux
function transfer_token() {
status_code=$(curl --header "Content-Type: application/json" \
--request POST --write-out %{http_code} --silent --output /dev/null \
--data '{"denom":"'"$DENOM"'","address":"'"$ADDRESS"'"}' \
$FAUCET_URL)
echo $status_code
}
if [[ $FAUCET_ENABLED == "false" ]];
then
echo "Faucet not enabled... skipping transfer token from faucet"
exit 0
fi
echo "Try to send tokens, if failed, wait for 5 seconds and try again"
max_tries=5
while [[ max_tries -gt 0 ]]
do
status_code=$(transfer_token)
if [[ "$status_code" -eq 200 ]]; then
echo "Successfully sent tokens"
exit 0
fi
echo "Failed to send tokens. Sleeping for 2 secs. Tries left $max_tries"
((max_tries--))
sleep 2
done
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# scripts/update-config.sh - Update configuration files for Sonr node
set -euo pipefail
# Source helper libraries
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/env.sh"
source "${SCRIPT_DIR}/lib/config.sh"
# Initialize environment
init_env
# Ensure chain directory exists
ensure_chain_dir
log_info "Updating configuration files for Sonr node"
# Configure node with standard settings
configure_node "$CHAIN_DIR" \
--rpc-port 26657 \
--rest-port 1317 \
--grpc-port 9090 \
--grpc-web-port 9091 \
--json-rpc-port 8545 8546 \
--rosetta-port 8080 \
--min-gas-prices "0${DENOM}" \
--pruning default \
--keyring-backend "$KEYRING_BACKEND" \
--chain-id "$CHAIN_ID" \
--output-format json
# Enable metrics if requested
if [[ "${METRICS:-false}" == "true" ]]; then
enable_metrics "$CHAIN_DIR/config/config.toml" 3600
fi
# Set consensus timeouts if provided
if [[ -n "${TIMEOUT_PROPOSE:-}" ]]; then
set_consensus_timeouts "$CHAIN_DIR/config/config.toml" \
"$TIMEOUT_PROPOSE" \
"${TIMEOUT_PREVOTE:-1s}" \
"${TIMEOUT_PRECOMMIT:-1s}" \
"${TIMEOUT_COMMIT:-5s}"
fi
log_success "Configuration update completed"
+23 -121
View File
@@ -1,130 +1,32 @@
#!/bin/bash
DENOM="${DENOM:=usnr}"
CHAIN_BIN="${CHAIN_BIN:=snrd}"
CHAIN_DIR="${CHAIN_DIR:=$HOME/.sonr}"
# scripts/update-genesis.sh - Update genesis.json with Sonr-specific parameters
set -eux
set -euo pipefail
ls "$CHAIN_DIR"/config
echo "Update genesis.json file with updated local params"
sed -i -e "s/\"stake\"/\"$DENOM\"/g" "$CHAIN_DIR"/config/genesis.json
sed -i "s/\"time_iota_ms\": \".*\"/\"time_iota_ms\": \"$TIME_IOTA_MS\"/" "$CHAIN_DIR"/config/genesis.json
echo "Update max gas param"
jq -r '.consensus.params.block.max_gas |= "100000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
echo "Update staking unbonding time and slashing jail time"
jq -r '.app_state.staking.params.unbonding_time |= "300s"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.slashing.params.downtime_jail_duration |= "60s"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
# overrides for older sdk versions, before 0.47
function gov_overrides_sdk_v46() {
jq -r '.app_state.gov.deposit_params.max_deposit_period |= "30s"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.deposit_params.min_deposit[0].amount |= "10"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.voting_params.voting_period |= "30s"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.tally_params.quorum |= "0.000000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.tally_params.threshold |= "0.000000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.tally_params.veto_threshold |= "0.000000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
}
# overrides for newer sdk versions, post 0.47
function gov_overrides_sdk_v47() {
jq -r '.app_state.gov.params.max_deposit_period |= "30s"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.params.min_deposit[0].amount |= "10"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.params.voting_period |= "30s"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.params.quorum |= "0.000000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.params.threshold |= "0.000000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.gov.params.veto_threshold |= "0.000000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
}
# EVM and feemarket configuration
if [ "$(jq -r '.app_state.evm' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r ".app_state.evm.params.evm_denom |= \"$DENOM\"" "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.evm.params.active_static_precompiles |= ["0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805"]' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
if [ "$(jq -r '.app_state.erc20' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r '.app_state.erc20.params.native_precompiles |= ["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r ".app_state.erc20.token_pairs |= [{\"contract_owner\":1,\"erc20_address\":\"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE\",\"denom\":\"$DENOM\",\"enabled\":true}]" "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
if [ "$(jq -r '.app_state.feemarket.params' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r '.app_state.feemarket.params.no_base_fee |= true' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.feemarket.params.base_fee |= "0.000000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
# Staking and mint configuration
if [ "$(jq -r '.app_state.staking' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r ".app_state.staking.params.bond_denom |= \"$DENOM\"" "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.staking.params.min_commission_rate |= "0.050000000000000000"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
if [ "$(jq -r '.app_state.mint' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r ".app_state.mint.params.mint_denom |= \"$DENOM\"" "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
if [ "$(jq -r '.app_state.crisis' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r ".app_state.crisis.constant_fee |= {\"denom\":\"$DENOM\",\"amount\":\"1000\"}" "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
# Token factory configuration
if [ "$(jq -r '.app_state.tokenfactory' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r '.app_state.tokenfactory.params.denom_creation_fee |= []' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
jq -r '.app_state.tokenfactory.params.denom_creation_gas_consume |= 100000' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
# ABCI configuration
if [ "$(jq -r '.consensus.params.abci' "$CHAIN_DIR"/config/genesis.json)" != "null" ]; then
jq -r '.consensus.params.abci.vote_extensions_enable_height |= "1"' "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
# Add CONSTITUTION.md to governance if it exists
# Look for CONSTITUTION.md in the git root directory (parent of scripts directory)
# Source helper libraries
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GIT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONSTITUTION_FILE="${GIT_ROOT}/CONSTITUTION.md"
source "${SCRIPT_DIR}/lib/env.sh"
source "${SCRIPT_DIR}/lib/genesis.sh"
if [ -f "$CONSTITUTION_FILE" ]; then
echo "Adding CONSTITUTION.md to governance module from: $CONSTITUTION_FILE"
CONSTITUTION_CONTENT=$(cat "$CONSTITUTION_FILE" | jq -Rs .)
jq -r ".app_state.gov.constitution = $CONSTITUTION_CONTENT" "$CHAIN_DIR"/config/genesis.json >/tmp/genesis.json
mv /tmp/genesis.json "$CHAIN_DIR"/config/genesis.json
fi
# Initialize environment
init_env
if [ "$(jq -r '.app_state.gov.params' "$CHAIN_DIR"/config/genesis.json)" == "null" ]; then
gov_overrides_sdk_v46
else
gov_overrides_sdk_v47
fi
# Ensure chain directory exists
ensure_chain_dir
log_info "Updating genesis.json file with Sonr parameters"
# Update genesis parameters using helper functions
update_genesis_params
# Add constitution if available
add_constitution
# Validate genesis file
validate_genesis
# Show node ID
$CHAIN_BIN tendermint show-node-id
log_success "Genesis update completed"