mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 09:21:39 +00:00
Feat/add networks (#1303)
* No commit suggestions generated * No commit suggestions generated * No commit suggestions generated
This commit is contained in:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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/)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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).
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
Executable
+59
@@ -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
|
||||
Executable
+27
@@ -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
|
||||
+86
-118
@@ -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
|
||||
# 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
|
||||
|
||||
# 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"
|
||||
log_info "Creating genesis for Sonr network: $CHAIN_ID"
|
||||
|
||||
# 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"
|
||||
# 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 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"
|
||||
# Add genesis accounts
|
||||
log_info "Adding genesis accounts..."
|
||||
|
||||
if [[ $FAUCET_ENABLED == "false" && $NUM_RELAYERS -gt "-1" ]]; then
|
||||
## Add relayers keys and delegate tokens
|
||||
# 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
|
||||
# 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"
|
||||
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
|
||||
|
||||
## 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"
|
||||
# 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 "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"
|
||||
# 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 "Output of gentx"
|
||||
cat "$CHAIN_DIR"/config/gentx/*.json | jq
|
||||
log_info "Genesis transaction output:"
|
||||
cat "$CHAIN_DIR/config/gentx"/*.json | jq
|
||||
|
||||
echo "Running collect-gentxs"
|
||||
# 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"
|
||||
|
||||
Executable
+84
@@ -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"
|
||||
Executable
+80
@@ -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"
|
||||
Executable
+26
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+124
-258
@@ -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
|
||||
# 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, 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}"
|
||||
|
||||
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"
|
||||
|
||||
Executable
+84
@@ -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"
|
||||
Executable
+36
@@ -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
|
||||
Executable
+48
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user