mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e041b4c9a | ||
|
|
6c6cec6326 | ||
|
|
f4c7b72b3e | ||
|
|
5dbcc455e8 | ||
|
|
181b10b6b2 | ||
|
|
4ddcd54558 | ||
|
|
f71193674b | ||
|
|
ad69f0ac80 | ||
|
|
28eba5a3f3 | ||
|
|
99167120dc | ||
|
|
93a5d8826d | ||
|
|
ae16bee0c1 | ||
|
|
6f93b63e4b | ||
|
|
3c682390a6 | ||
|
|
85304fd8b4 | ||
|
|
be4e4b8367 | ||
|
|
88ca486108 | ||
|
|
30635ba397 | ||
|
|
9db77a2bf4 |
@@ -0,0 +1,18 @@
|
|||||||
|
[scopes]
|
||||||
|
ante = ["app/ante"]
|
||||||
|
api = ["api/dex", "api/did", "api/dwn", "api/svc", "api"]
|
||||||
|
app = ["app/context", "app/decorators", "app/params", "app/upgrades", "app"]
|
||||||
|
chains = ["chains"]
|
||||||
|
ci = ["Makefile", ".github"]
|
||||||
|
cli = ["app/commands"]
|
||||||
|
cmd = ["cmd/snrd", "cmd"]
|
||||||
|
config = ["networks/localnet", "networks/testnet", "networks"]
|
||||||
|
deps = ["go.sum", "go.mod"]
|
||||||
|
dex = ["proto/dex", "x/dex"]
|
||||||
|
did = ["proto/did", "x/did"]
|
||||||
|
docs = ["docs"]
|
||||||
|
dwn = ["proto/dwn", "x/dwn"]
|
||||||
|
proto = ["proto"]
|
||||||
|
scripts = ["scripts"]
|
||||||
|
svc = ["proto/svc", "x/svc"]
|
||||||
|
test = ["test/e2e", "test/integration", "test/oauth", "test"]
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"scope": "deps",
|
|
||||||
"path": "go.sum"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "deps",
|
|
||||||
"path": "go.mod"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "api",
|
|
||||||
"path": "api"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ante",
|
|
||||||
"path": "app/ante"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "cli",
|
|
||||||
"path": "app/commands"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/context"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/decorators"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/params"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app/upgrades"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "app",
|
|
||||||
"path": "app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "chains",
|
|
||||||
"path": "chains"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "cmd",
|
|
||||||
"path": "cmd/snrd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "cmd",
|
|
||||||
"path": "cmd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "docs",
|
|
||||||
"path": "docs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "config",
|
|
||||||
"path": "networks/localnet"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "config",
|
|
||||||
"path": "networks/testnet"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "config",
|
|
||||||
"path": "networks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dex",
|
|
||||||
"path": "proto/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "did",
|
|
||||||
"path": "proto/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dwn",
|
|
||||||
"path": "proto/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "svc",
|
|
||||||
"path": "proto/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "proto",
|
|
||||||
"path": "proto"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "scripts",
|
|
||||||
"path": "scripts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test/e2e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test/integration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test/oauth"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "test",
|
|
||||||
"path": "test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dex",
|
|
||||||
"path": "x/dex"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "did",
|
|
||||||
"path": "x/did"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "dwn",
|
|
||||||
"path": "x/dwn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "svc",
|
|
||||||
"path": "x/svc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ci",
|
|
||||||
"path": "Makefile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "ci",
|
|
||||||
"path": ".github"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
report:
|
report:
|
||||||
name: Report CI Status
|
name: Report CI Status
|
||||||
runs-on: builder
|
runs-on: tenki-standard-autoscale
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Report Failure
|
- name: Report Failure
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
name: Validate & Test
|
name: Validate & Test
|
||||||
runs-on: builder # Self-hosted runner
|
runs-on: tenki-standard-autoscale
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Required for github-env.sh comparisons
|
fetch-depth: 0 # Required for github-env.sh comparisons
|
||||||
# Cache can be local on self-hosted runner for better performance
|
# Cache can be local on self-hosted runner for better performance
|
||||||
|
|||||||
@@ -121,3 +121,5 @@ tmp-openapi-gen/**/*.json
|
|||||||
# Allow the final combined openapi.yml
|
# Allow the final combined openapi.yml
|
||||||
!docs/static/openapi.yml
|
!docs/static/openapi.yml
|
||||||
.mcp.json
|
.mcp.json
|
||||||
|
node_modules
|
||||||
|
.claude
|
||||||
|
|||||||
@@ -253,6 +253,19 @@ test-e2e-all:
|
|||||||
@gum log --level info "Running all e2e tests"
|
@gum log --level info "Running all e2e tests"
|
||||||
@cd test/e2e && go test -race -v ./tests/...
|
@cd test/e2e && go test -race -v ./tests/...
|
||||||
|
|
||||||
|
e2e-usdc-swap-did:
|
||||||
|
@gum log --level info "Running USDC Swap E2E tests with DID integration"
|
||||||
|
@gum log --level warn "Note: These tests require manual setup. See test/e2e/usdc-swap-did/README.md"
|
||||||
|
@cd test/e2e/usdc-swap-did && make test
|
||||||
|
|
||||||
|
e2e-usdc-swap-did-setup:
|
||||||
|
@gum log --level info "Running USDC Swap E2E test setup..."
|
||||||
|
@cd test/e2e/usdc-swap-did && bash setup.sh
|
||||||
|
|
||||||
|
e2e-usdc-swap-did-verbose:
|
||||||
|
@gum log --level info "Running USDC Swap E2E tests (verbose mode)"
|
||||||
|
@cd test/e2e/usdc-swap-did && make test-verbose
|
||||||
|
|
||||||
test-build-snrd: build
|
test-build-snrd: build
|
||||||
@ls -la build/snrd
|
@ls -la build/snrd
|
||||||
@chmod +x build/snrd
|
@chmod +x build/snrd
|
||||||
@@ -306,7 +319,7 @@ test-proto:
|
|||||||
test-benchmark:
|
test-benchmark:
|
||||||
@go test -mod=readonly -bench=. ./...
|
@go test -mod=readonly -bench=. ./...
|
||||||
|
|
||||||
.PHONY: test test-all test-unit test-race test-cover test-tdd test-module test-benchmark
|
.PHONY: test test-all test-unit test-race test-cover test-tdd test-module test-benchmark test-e2e test-e2e-all e2e-usdc-swap-did e2e-usdc-swap-did-setup e2e-usdc-swap-did-verbose
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
### Protobuf ###
|
### Protobuf ###
|
||||||
@@ -402,6 +415,11 @@ help:
|
|||||||
@gum log --level info " test-e2e-all Run all e2e tests"
|
@gum log --level info " test-e2e-all Run all e2e tests"
|
||||||
@gum log --level info " test-module Test specific module (MODULE=did|dwn|svc)"
|
@gum log --level info " test-module Test specific module (MODULE=did|dwn|svc)"
|
||||||
@gum log --level info ""
|
@gum log --level info ""
|
||||||
|
@gum log --level info "🔄 E2E Test Suites:"
|
||||||
|
@gum log --level info " e2e-usdc-swap-did Run USDC swap E2E tests"
|
||||||
|
@gum log --level info " e2e-usdc-swap-did-setup Interactive setup for USDC swap tests"
|
||||||
|
@gum log --level info " e2e-usdc-swap-did-verbose Run USDC swap tests with verbose output"
|
||||||
|
@gum log --level info ""
|
||||||
@gum log --level info "📚 Module Testing Examples:"
|
@gum log --level info "📚 Module Testing Examples:"
|
||||||
@gum log --level info " make test-module MODULE=did # Test DID module"
|
@gum log --level info " make test-module MODULE=did # Test DID module"
|
||||||
@gum log --level info " make test-module MODULE=dwn VARIANT=cover # DWN with coverage"
|
@gum log --level info " make test-module MODULE=dwn VARIANT=cover # DWN with coverage"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
svrcmd "github.com/cosmos/cosmos-sdk/server/cmd"
|
svrcmd "github.com/cosmos/cosmos-sdk/server/cmd"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
@@ -35,3 +36,25 @@ func SetBech32Prefixes(config *sdk.Config) {
|
|||||||
config.SetBech32PrefixForValidator(app.Bech32PrefixValAddr, app.Bech32PrefixValPub)
|
config.SetBech32PrefixForValidator(app.Bech32PrefixValAddr, app.Bech32PrefixValPub)
|
||||||
config.SetBech32PrefixForConsensusNode(app.Bech32PrefixConsAddr, app.Bech32PrefixConsPub)
|
config.SetBech32PrefixForConsensusNode(app.Bech32PrefixConsAddr, app.Bech32PrefixConsPub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sonrLogo() string {
|
||||||
|
lines := []string{
|
||||||
|
"[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m░[0m [38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m░[0m [38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m░[0m [38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m░[0m [38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m█[0m[38;2;153;255;255m░[0m",
|
||||||
|
"[38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m [38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m [38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m [38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m [38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m [38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m [38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m [38;2;119;233;242m█[0m[38;2;119;233;242m█[0m[38;2;119;233;242m░[0m",
|
||||||
|
"[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m░[0m [38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m░[0m [38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m░[0m [38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m░[0m [38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m░[0m [38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m░[0m [38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m█[0m[38;2;85;211;230m░[0m",
|
||||||
|
" [38;2;51;189;217m█[0m[38;2;51;189;217m█[0m[38;2;51;189;217m░[0m [38;2;51;189;217m█[0m[38;2;51;189;217m█[0m[38;2;51;189;217m░[0m [38;2;51;189;217m█[0m[38;2;51;189;217m█[0m[38;2;51;189;217m░[0m [38;2;51;189;217m█[0m[38;2;51;189;217m█[0m[38;2;51;189;217m░[0m [38;2;51;189;217m█[0m[38;2;51;189;217m█[0m[38;2;51;189;217m░[0m [38;2;51;189;217m█[0m[38;2;51;189;217m█[0m[38;2;51;189;217m░[0m [38;2;51;189;217m█[0m[38;2;51;189;217m█[0m[38;2;51;189;217m░[0m ",
|
||||||
|
"[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m░[0m [38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m░[0m [38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m░[0m [38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m░[0m [38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m░[0m [38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m█[0m[38;2;17;168;205m░[0m",
|
||||||
|
}
|
||||||
|
var logo string
|
||||||
|
// Add top padding
|
||||||
|
logo += "\n"
|
||||||
|
for _, line := range lines {
|
||||||
|
// Replace [ with \x1b[ to make valid ANSI escape sequences
|
||||||
|
line = strings.ReplaceAll(line, "[", "\x1b[")
|
||||||
|
// Add horizontal padding
|
||||||
|
logo += " " + line + "\n"
|
||||||
|
}
|
||||||
|
// Add bottom padding
|
||||||
|
logo += "\n"
|
||||||
|
return logo
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func NewRootCmd() *cobra.Command {
|
|||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: version.AppName,
|
Use: version.AppName,
|
||||||
Short: version.AppName + " Daemon (server)",
|
Short: version.AppName + " Daemon (server)",
|
||||||
|
Long: sonrLogo(),
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
// set the default command outputs
|
// set the default command outputs
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,8 @@
|
|||||||
"jq@latest",
|
"jq@latest",
|
||||||
"trunk@latest",
|
"trunk@latest",
|
||||||
"mods@latest",
|
"mods@latest",
|
||||||
"yq@latest"
|
"yq@latest",
|
||||||
|
"dasel@latest"
|
||||||
],
|
],
|
||||||
"env": {},
|
"env": {},
|
||||||
"shell": {
|
"shell": {
|
||||||
|
|||||||
+48
@@ -1,6 +1,54 @@
|
|||||||
{
|
{
|
||||||
"lockfile_version": "1",
|
"lockfile_version": "1",
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"dasel@latest": {
|
||||||
|
"last_modified": "2025-10-07T08:41:47Z",
|
||||||
|
"resolved": "github:NixOS/nixpkgs/bce5fe2bb998488d8e7e7856315f90496723793c#dasel",
|
||||||
|
"source": "devbox-search",
|
||||||
|
"version": "2.8.1",
|
||||||
|
"systems": {
|
||||||
|
"aarch64-darwin": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/kahwgfqz0x48aj3ikrd963pinkjsgi1p-dasel-2.8.1",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/kahwgfqz0x48aj3ikrd963pinkjsgi1p-dasel-2.8.1"
|
||||||
|
},
|
||||||
|
"aarch64-linux": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/gcnpff6qc8r8rz6paphc3j2q20hhys5r-dasel-2.8.1",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/gcnpff6qc8r8rz6paphc3j2q20hhys5r-dasel-2.8.1"
|
||||||
|
},
|
||||||
|
"x86_64-darwin": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/c97l94fqsic4nqrgrzrn5fjbw18lggjw-dasel-2.8.1",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/c97l94fqsic4nqrgrzrn5fjbw18lggjw-dasel-2.8.1"
|
||||||
|
},
|
||||||
|
"x86_64-linux": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/5494996bmd4nqv17l4ji9r3r2n5b1aqj-dasel-2.8.1",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/5494996bmd4nqv17l4ji9r3r2n5b1aqj-dasel-2.8.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"docker@latest": {
|
"docker@latest": {
|
||||||
"last_modified": "2025-08-05T11:35:34Z",
|
"last_modified": "2025-08-05T11:35:34Z",
|
||||||
"resolved": "github:NixOS/nixpkgs/a683adc19ff5228af548c6539dbc3440509bfed3#docker",
|
"resolved": "github:NixOS/nixpkgs/a683adc19ff5228af548c6539dbc3440509bfed3#docker",
|
||||||
|
|||||||
@@ -32,26 +32,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- sonr-network
|
- 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:
|
snrd:
|
||||||
container_name: snrd
|
container_name: snrd
|
||||||
image: onsonr/snrd:latest
|
image: onsonr/snrd:latest
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Migrating from Centralized to Decentralized Authentication"
|
|
||||||
description: "A comprehensive guide to migrating from centralized 8787 endpoints to decentralized OIDC authentication with WebAuthn"
|
|
||||||
sidebarTitle: "Decentralized Authentication"
|
|
||||||
icon: "key"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Migrating from Centralized to Decentralized Authentication
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Sonr is transitioning from a centralized authentication model using 8787 endpoints to a fully decentralized, privacy-preserving authentication system leveraging OpenID Connect (OIDC), Self-Issued OpenID Provider (SIOP), and WebAuthn technologies.
|
|
||||||
|
|
||||||
### Key Improvements
|
|
||||||
|
|
||||||
- **Decentralization**: Move from centralized identity management to self-sovereign identity
|
|
||||||
- **WebAuthn Integration**: Hardware-backed, phishing-resistance authentication
|
|
||||||
- **Gasless Onboarding**: Zero-cost user registration and transactions
|
|
||||||
- **Automatic Vault Creation**: Seamless user experience with instant identity setup
|
|
||||||
- **Enhanced Privacy**: DID-based authentication with verifiable presentations
|
|
||||||
|
|
||||||
## Architecture Comparison
|
|
||||||
|
|
||||||
### Old Architecture: Centralized 8787 Endpoints
|
|
||||||
|
|
||||||
- Centralized authentication server
|
|
||||||
- Fixed credential storage
|
|
||||||
- Limited authentication methods
|
|
||||||
- Higher security risks
|
|
||||||
|
|
||||||
### New Architecture: Decentralized OIDC Provider
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[User Device] --> B{WebAuthn Registration}
|
|
||||||
B --> |Credential Generation| C[Blockchain Bridge]
|
|
||||||
C --> |Broadcast Credential| D[Sonr Blockchain]
|
|
||||||
D --> |Create DID| E[User's Decentralized Identity]
|
|
||||||
E --> |SIOP Flow| F[OIDC Provider]
|
|
||||||
F --> |Verifiable Presentation| G[Service Provider]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Steps
|
|
||||||
|
|
||||||
### 1. Prerequisites
|
|
||||||
|
|
||||||
- Go 1.24.1+
|
|
||||||
- Cosmos SDK v0.50.14
|
|
||||||
- WebAuthn-compatible browser/device
|
|
||||||
- Updated Sonr SDK
|
|
||||||
|
|
||||||
### 2. Configuration Changes
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```bash title="Old Configuration"
|
|
||||||
# Centralized auth endpoints
|
|
||||||
AUTH_ENDPOINT=https://8787.sonr.io/auth
|
|
||||||
AUTH_CLIENT_ID=legacy-client-id
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash title="New Configuration"
|
|
||||||
# Decentralized OIDC configuration
|
|
||||||
OIDC_PROVIDER_URL=https://oidc.sonr.network
|
|
||||||
WEBAUTHN_ORIGIN=https://your-app.com
|
|
||||||
DID_RESOLVER_URL=https://did.sonr.network
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
### 3. Endpoint Migration
|
|
||||||
|
|
||||||
| Old Endpoint | New Endpoint | Changes |
|
|
||||||
| ---------------- | -------------------- | ----------------------- |
|
|
||||||
| `/auth/login` | `/oidc/authorize` | OIDC authorization flow |
|
|
||||||
| `/auth/register` | `/webauthn/register` | WebAuthn registration |
|
|
||||||
| `/auth/token` | `/oidc/token` | Token issuance via SIOP |
|
|
||||||
|
|
||||||
### 4. API Changes
|
|
||||||
|
|
||||||
#### Request Format
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```json title="Legacy Request"
|
|
||||||
{
|
|
||||||
"username": "user@example.com",
|
|
||||||
"password": "legacy-password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json title="New WebAuthn Request"
|
|
||||||
{
|
|
||||||
"challenge": "base64-encoded-challenge",
|
|
||||||
"attestation": {
|
|
||||||
"type": "public-key",
|
|
||||||
"id": "credential-id",
|
|
||||||
"rawId": "base64-encoded-raw-credential",
|
|
||||||
"response": {
|
|
||||||
"clientDataJSON": "...",
|
|
||||||
"attestationObject": "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
### 5. WebAuthn Integration
|
|
||||||
|
|
||||||
#### Registration Flow
|
|
||||||
|
|
||||||
1. Generate registration challenge
|
|
||||||
2. Create WebAuthn credential
|
|
||||||
3. Broadcast credential to blockchain
|
|
||||||
4. Automatically create user vault
|
|
||||||
5. Issue decentralized identifier (DID)
|
|
||||||
|
|
||||||
#### Code Example
|
|
||||||
|
|
||||||
```go title="WebAuthn Registration Handler"
|
|
||||||
func (s *Server) HandleWebAuthnRegistration(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 1. Validate WebAuthn attestation
|
|
||||||
credential, err := s.webAuthnService.VerifyRegistration(attestationData)
|
|
||||||
|
|
||||||
// 2. Broadcast to blockchain
|
|
||||||
didDoc, err := s.blockchainBridge.CreateDID(credential)
|
|
||||||
|
|
||||||
// 3. Create user vault
|
|
||||||
vault, err := s.vaultService.CreateVault(didDoc)
|
|
||||||
|
|
||||||
// 4. Issue OIDC token
|
|
||||||
token := s.oidcService.IssueToken(didDoc)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. SIOP (Self-Issued OpenID Provider)
|
|
||||||
|
|
||||||
#### Key Concepts
|
|
||||||
|
|
||||||
- Verifiable Presentations
|
|
||||||
- Decentralized Identifiers (DIDs)
|
|
||||||
- User-controlled authentication
|
|
||||||
|
|
||||||
### 7. Error Handling
|
|
||||||
|
|
||||||
#### Migration Errors
|
|
||||||
|
|
||||||
| Error Code | Description | Mitigation |
|
|
||||||
| ------------------------ | ---------------------- | -------------------- |
|
|
||||||
| `AUTH_LEGACY_DEPRECATED` | Legacy auth method | Upgrade client |
|
|
||||||
| `WEBAUTHN_UNSUPPORTED` | Device incompatibility | Use alternative auth |
|
|
||||||
| `DID_RESOLUTION_FAILED` | Identity verification | Retry registration |
|
|
||||||
|
|
||||||
### 8. Testing Migration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate WebAuthn registration
|
|
||||||
sonr webauthn test-registration
|
|
||||||
|
|
||||||
# Verify OIDC provider
|
|
||||||
sonr oidc validate-provider
|
|
||||||
|
|
||||||
# Check DID resolution
|
|
||||||
sonr did resolve did:sonr:example
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. Breaking Changes
|
|
||||||
|
|
||||||
- Legacy password authentication removed
|
|
||||||
- Token format changed to JWT with DID claims
|
|
||||||
- New client libraries required
|
|
||||||
- WebAuthn mandatory for registration
|
|
||||||
|
|
||||||
## Rollback Procedure
|
|
||||||
|
|
||||||
If issues arise:
|
|
||||||
|
|
||||||
1. Maintain legacy user mappings
|
|
||||||
2. Provide fallback authentication
|
|
||||||
3. Gradual, opt-in migration
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This migration represents a significant leap in authentication security and user privacy. By adopting WebAuthn, SIOP, and blockchain-backed identities, we're creating a more robust, user-controlled authentication ecosystem.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- [Sonr Developer Docs](/docs)
|
|
||||||
- [WebAuthn Specification](https://www.w3.org/TR/webauthn-2/)
|
|
||||||
- [OIDC Community](https://openid.net/developers/specs/)
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
---
|
|
||||||
title: "DID Generation and Management"
|
|
||||||
description: "Learn how to generate and manage Decentralized Identifiers (DIDs) using advanced crypto interfaces"
|
|
||||||
sidebarTitle: "Identity Generation"
|
|
||||||
icon: "signature"
|
|
||||||
---
|
|
||||||
|
|
||||||
# DID Generation Patterns
|
|
||||||
|
|
||||||
The Sonr project provides a robust and flexible DID (Decentralized Identifier) generation system supporting multiple key types and cryptographic methods.
|
|
||||||
|
|
||||||
## Supported Key Types
|
|
||||||
|
|
||||||
The `didkey.go` interface supports the following key types:
|
|
||||||
|
|
||||||
- RSA Public Keys
|
|
||||||
- Ed25519 Public Keys
|
|
||||||
- Secp256k1 Public Keys
|
|
||||||
|
|
||||||
## Basic DID Generation
|
|
||||||
|
|
||||||
### Creating a DID from a Public Key
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/sonr-io/crypto/keys"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate a new key pair
|
|
||||||
privateKey, publicKey, err := crypto.GenerateKeyPair(crypto.Ed25519, -1)
|
|
||||||
|
|
||||||
// Create a DID
|
|
||||||
did, err := keys.NewDID(publicKey)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parsing an Existing DID
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Parse a DID string
|
|
||||||
did, err := keys.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGTsyDFWYviJr4")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced DID Operations
|
|
||||||
|
|
||||||
### Address Derivation
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Get a blockchain-compatible address from a DID
|
|
||||||
address, err := did.Address()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compressed Public Key Retrieval
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Get a compressed public key (for Secp256k1)
|
|
||||||
compressedPubKey, err := did.CompressedPubKey()
|
|
||||||
```
|
|
||||||
|
|
||||||
## MPC Enclave DID Generation
|
|
||||||
|
|
||||||
For MPC (Multi-Party Computation) enclaves, use a specialized method:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Create a DID from MPC enclave public key bytes
|
|
||||||
did, err := keys.NewFromMPCPubKey(enclavePublicKeyBytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
## DID Validation
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Validate a DID format
|
|
||||||
err := keys.ValidateFormat("did:key:example123")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multicodec Support
|
|
||||||
|
|
||||||
The library supports various multicodec prefixes for different key types:
|
|
||||||
|
|
||||||
- RSA: `0x1205`
|
|
||||||
- Ed25519: `0xed`
|
|
||||||
- Secp256k1: `0xe7`
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- Always generate keys using cryptographically secure methods
|
|
||||||
- Validate DIDs before using them in critical operations
|
|
||||||
- Use the appropriate key type for your security requirements
|
|
||||||
|
|
||||||
## Example: Complete DID Workflow
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/sonr-io/crypto/keys"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Generate a new key pair
|
|
||||||
privateKey, publicKey, err := crypto.GenerateKeyPair(crypto.Ed25519, -1)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a DID
|
|
||||||
did, err := keys.NewDID(publicKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the DID string
|
|
||||||
didString := did.String()
|
|
||||||
fmt.Println("Generated DID:", didString)
|
|
||||||
|
|
||||||
// Derive blockchain address
|
|
||||||
address, err := did.Address()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println("Blockchain Address:", address)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Compatibility and Interoperability
|
|
||||||
|
|
||||||
The Sonr DID implementation follows the W3C DID specification, ensuring broad compatibility with other decentralized identity systems.
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Migrating from Centralized to Decentralized Authentication"
|
|
||||||
description: "A comprehensive guide to migrating from centralized 8787 endpoints to decentralized OIDC authentication with WebAuthn"
|
|
||||||
sidebarTitle: "Decentralized Authentication"
|
|
||||||
icon: "key"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Migrating from Centralized to Decentralized Authentication
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Sonr is transitioning from a centralized authentication model using 8787 endpoints to a fully decentralized, privacy-preserving authentication system leveraging OpenID Connect (OIDC), Self-Issued OpenID Provider (SIOP), and WebAuthn technologies.
|
|
||||||
|
|
||||||
### Key Improvements
|
|
||||||
|
|
||||||
- **Decentralization**: Move from centralized identity management to self-sovereign identity
|
|
||||||
- **WebAuthn Integration**: Hardware-backed, phishing-resistance authentication
|
|
||||||
- **Gasless Onboarding**: Zero-cost user registration and transactions
|
|
||||||
- **Automatic Vault Creation**: Seamless user experience with instant identity setup
|
|
||||||
- **Enhanced Privacy**: DID-based authentication with verifiable presentations
|
|
||||||
|
|
||||||
## Architecture Comparison
|
|
||||||
|
|
||||||
### Old Architecture: Centralized 8787 Endpoints
|
|
||||||
|
|
||||||
- Centralized authentication server
|
|
||||||
- Fixed credential storage
|
|
||||||
- Limited authentication methods
|
|
||||||
- Higher security risks
|
|
||||||
|
|
||||||
### New Architecture: Decentralized OIDC Provider
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[User Device] --> B{WebAuthn Registration}
|
|
||||||
B --> |Credential Generation| C[Blockchain Bridge]
|
|
||||||
C --> |Broadcast Credential| D[Sonr Blockchain]
|
|
||||||
D --> |Create DID| E[User's Decentralized Identity]
|
|
||||||
E --> |SIOP Flow| F[OIDC Provider]
|
|
||||||
F --> |Verifiable Presentation| G[Service Provider]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Steps
|
|
||||||
|
|
||||||
### 1. Prerequisites
|
|
||||||
|
|
||||||
- Go 1.24.1+
|
|
||||||
- Cosmos SDK v0.50.14
|
|
||||||
- WebAuthn-compatible browser/device
|
|
||||||
- Updated Sonr SDK
|
|
||||||
|
|
||||||
### 2. Configuration Changes
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```bash title="Old Configuration"
|
|
||||||
# Centralized auth endpoints
|
|
||||||
AUTH_ENDPOINT=https://8787.sonr.io/auth
|
|
||||||
AUTH_CLIENT_ID=legacy-client-id
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash title="New Configuration"
|
|
||||||
# Decentralized OIDC configuration
|
|
||||||
OIDC_PROVIDER_URL=https://oidc.sonr.network
|
|
||||||
WEBAUTHN_ORIGIN=https://your-app.com
|
|
||||||
DID_RESOLVER_URL=https://did.sonr.network
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
### 3. Endpoint Migration
|
|
||||||
|
|
||||||
| Old Endpoint | New Endpoint | Changes |
|
|
||||||
| ---------------- | -------------------- | ----------------------- |
|
|
||||||
| `/auth/login` | `/oidc/authorize` | OIDC authorization flow |
|
|
||||||
| `/auth/register` | `/webauthn/register` | WebAuthn registration |
|
|
||||||
| `/auth/token` | `/oidc/token` | Token issuance via SIOP |
|
|
||||||
|
|
||||||
### 4. API Changes
|
|
||||||
|
|
||||||
#### Request Format
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```json title="Legacy Request"
|
|
||||||
{
|
|
||||||
"username": "user@example.com",
|
|
||||||
"password": "legacy-password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json title="New WebAuthn Request"
|
|
||||||
{
|
|
||||||
"challenge": "base64-encoded-challenge",
|
|
||||||
"attestation": {
|
|
||||||
"type": "public-key",
|
|
||||||
"id": "credential-id",
|
|
||||||
"rawId": "base64-encoded-raw-credential",
|
|
||||||
"response": {
|
|
||||||
"clientDataJSON": "...",
|
|
||||||
"attestationObject": "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
### 5. WebAuthn Integration
|
|
||||||
|
|
||||||
#### Registration Flow
|
|
||||||
|
|
||||||
1. Generate registration challenge
|
|
||||||
2. Create WebAuthn credential
|
|
||||||
3. Broadcast credential to blockchain
|
|
||||||
4. Automatically create user vault
|
|
||||||
5. Issue decentralized identifier (DID)
|
|
||||||
|
|
||||||
#### Code Example
|
|
||||||
|
|
||||||
```go title="WebAuthn Registration Handler"
|
|
||||||
func (s *Server) HandleWebAuthnRegistration(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 1. Validate WebAuthn attestation
|
|
||||||
credential, err := s.webAuthnService.VerifyRegistration(attestationData)
|
|
||||||
|
|
||||||
// 2. Broadcast to blockchain
|
|
||||||
didDoc, err := s.blockchainBridge.CreateDID(credential)
|
|
||||||
|
|
||||||
// 3. Create user vault
|
|
||||||
vault, err := s.vaultService.CreateVault(didDoc)
|
|
||||||
|
|
||||||
// 4. Issue OIDC token
|
|
||||||
token := s.oidcService.IssueToken(didDoc)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. SIOP (Self-Issued OpenID Provider)
|
|
||||||
|
|
||||||
#### Key Concepts
|
|
||||||
|
|
||||||
- Verifiable Presentations
|
|
||||||
- Decentralized Identifiers (DIDs)
|
|
||||||
- User-controlled authentication
|
|
||||||
|
|
||||||
### 7. Error Handling
|
|
||||||
|
|
||||||
#### Migration Errors
|
|
||||||
|
|
||||||
| Error Code | Description | Mitigation |
|
|
||||||
| ------------------------ | ---------------------- | -------------------- |
|
|
||||||
| `AUTH_LEGACY_DEPRECATED` | Legacy auth method | Upgrade client |
|
|
||||||
| `WEBAUTHN_UNSUPPORTED` | Device incompatibility | Use alternative auth |
|
|
||||||
| `DID_RESOLUTION_FAILED` | Identity verification | Retry registration |
|
|
||||||
|
|
||||||
### 8. Testing Migration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate WebAuthn registration
|
|
||||||
sonr webauthn test-registration
|
|
||||||
|
|
||||||
# Verify OIDC provider
|
|
||||||
sonr oidc validate-provider
|
|
||||||
|
|
||||||
# Check DID resolution
|
|
||||||
sonr did resolve did:sonr:example
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. Breaking Changes
|
|
||||||
|
|
||||||
- Legacy password authentication removed
|
|
||||||
- Token format changed to JWT with DID claims
|
|
||||||
- New client libraries required
|
|
||||||
- WebAuthn mandatory for registration
|
|
||||||
|
|
||||||
## Rollback Procedure
|
|
||||||
|
|
||||||
If issues arise:
|
|
||||||
|
|
||||||
1. Maintain legacy user mappings
|
|
||||||
2. Provide fallback authentication
|
|
||||||
3. Gradual, opt-in migration
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This migration represents a significant leap in authentication security and user privacy. By adopting WebAuthn, SIOP, and blockchain-backed identities, we're creating a more robust, user-controlled authentication ecosystem.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- [Sonr Developer Docs](/docs)
|
|
||||||
- [WebAuthn Specification](https://www.w3.org/TR/webauthn-2/)
|
|
||||||
- [OIDC Community](https://openid.net/developers/specs/)
|
|
||||||
+396
@@ -0,0 +1,396 @@
|
|||||||
|
# sonrctl 🚀
|
||||||
|
|
||||||
|
**Blazingly fast CLI for managing Sonr blockchain nodes, validators, and networks**
|
||||||
|
|
||||||
|
Built with [Bun](https://bun.sh) - the all-in-one JavaScript runtime.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ⚡ **Blazingly Fast** - Built on Bun for instant startup and execution
|
||||||
|
- 🎯 **Simple API** - Clean, intuitive commands for node management
|
||||||
|
- 🐳 **Docker Integration** - Seamless container orchestration for testnets
|
||||||
|
- 💾 **SQLite Config** - Lightweight, fast configuration management
|
||||||
|
- 🎨 **Beautiful Output** - Colored, formatted CLI output
|
||||||
|
- 🔧 **Node Management** - Initialize, start, stop, and monitor nodes
|
||||||
|
- 🌐 **Network Operations** - Full testnet deployment and management
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Install Bun (if not already installed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install sonrctl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/sonr-io/sonr.git
|
||||||
|
cd sonr
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Link the CLI globally
|
||||||
|
bun link
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install snrd binary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sonrctl install latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show help
|
||||||
|
sonrctl help
|
||||||
|
|
||||||
|
# Initialize a validator node
|
||||||
|
sonrctl init validator val-naruto --chain-id sonrtest_1-1
|
||||||
|
|
||||||
|
# Start the entire testnet network
|
||||||
|
sonrctl start network --detach
|
||||||
|
|
||||||
|
# Check network status
|
||||||
|
sonrctl status network
|
||||||
|
|
||||||
|
# Stop the network
|
||||||
|
sonrctl stop network
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `sonrctl install [version]`
|
||||||
|
|
||||||
|
Install or update the snrd binary.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install latest version
|
||||||
|
sonrctl install latest
|
||||||
|
|
||||||
|
# Install specific version (coming soon)
|
||||||
|
sonrctl install v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sonrctl init <type> <name> [options]`
|
||||||
|
|
||||||
|
Initialize a new node configuration.
|
||||||
|
|
||||||
|
**Types:** `validator`, `sentry`, `full`
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--chain-id <id>` - Chain ID (default: sonrtest_1-1)
|
||||||
|
- `--home <path>` - Home directory (default: ~/.sonr/<name>)
|
||||||
|
- `--rpc-port <port>` - RPC port (default: 26657)
|
||||||
|
- `--rest-port <port>` - REST API port (default: 1317)
|
||||||
|
- `--grpc-port <port>` - gRPC port (default: 9090)
|
||||||
|
- `--grpc-web-port <port>` - gRPC-Web port (default: 9091)
|
||||||
|
- `--json-rpc-port <port>` - JSON-RPC port (default: 8545)
|
||||||
|
- `--json-rpc-ws-port <port>` - JSON-RPC WebSocket port (default: 8546)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize a validator
|
||||||
|
sonrctl init validator val-naruto
|
||||||
|
|
||||||
|
# Initialize a sentry with custom home
|
||||||
|
sonrctl init sentry sentry-naruto --home ~/.sonr/custom-sentry
|
||||||
|
|
||||||
|
# Initialize with custom ports
|
||||||
|
sonrctl init validator my-val --rpc-port 26658 --rest-port 1318
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sonrctl start <target> [options]`
|
||||||
|
|
||||||
|
Start a node or the entire network.
|
||||||
|
|
||||||
|
**Targets:**
|
||||||
|
- `<node-name>` - Start a specific registered node
|
||||||
|
- `network` - Start the entire testnet using docker-compose
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--detach`, `-d` - Run in background (network only)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start a specific node
|
||||||
|
sonrctl start val-naruto
|
||||||
|
|
||||||
|
# Start network in foreground
|
||||||
|
sonrctl start network
|
||||||
|
|
||||||
|
# Start network in background
|
||||||
|
sonrctl start network --detach
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sonrctl stop <target>`
|
||||||
|
|
||||||
|
Stop a node, network, or all containers.
|
||||||
|
|
||||||
|
**Targets:**
|
||||||
|
- `<node-name>` - Stop a specific container
|
||||||
|
- `network` - Stop the entire testnet
|
||||||
|
- `all` - Stop all running containers
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop a specific node
|
||||||
|
sonrctl stop val-naruto
|
||||||
|
|
||||||
|
# Stop the network
|
||||||
|
sonrctl stop network
|
||||||
|
|
||||||
|
# Stop all containers
|
||||||
|
sonrctl stop all
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sonrctl status [target]`
|
||||||
|
|
||||||
|
Check status of nodes and network.
|
||||||
|
|
||||||
|
**Targets:**
|
||||||
|
- (none) - Show overall status
|
||||||
|
- `<node-name>` - Show specific node status
|
||||||
|
- `network` - Show network status
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show overall status
|
||||||
|
sonrctl status
|
||||||
|
|
||||||
|
# Show specific node status
|
||||||
|
sonrctl status val-naruto
|
||||||
|
|
||||||
|
# Show network status
|
||||||
|
sonrctl status network
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sonrctl config <command> [args]`
|
||||||
|
|
||||||
|
Manage sonrctl configuration.
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `list` - List all configuration
|
||||||
|
- `get <key>` - Get a specific value
|
||||||
|
- `set <key> <value>` - Set a configuration value
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all configuration
|
||||||
|
sonrctl config list
|
||||||
|
|
||||||
|
# Get chain ID
|
||||||
|
sonrctl config get chain_id
|
||||||
|
|
||||||
|
# Set home directory
|
||||||
|
sonrctl config set home ~/.sonr-custom
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
sonrctl stores its configuration in:
|
||||||
|
- **Config Database:** `~/.config/sonr/config.db`
|
||||||
|
- **Node Home:** `~/.sonr/` (default)
|
||||||
|
|
||||||
|
The SQLite database stores:
|
||||||
|
- Global configuration (chain ID, binary path, etc.)
|
||||||
|
- Registered nodes
|
||||||
|
- Node metadata
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # Main CLI entry point
|
||||||
|
├── types.ts # TypeScript type definitions
|
||||||
|
├── commands/ # Command implementations
|
||||||
|
│ ├── install.ts # Install snrd binary
|
||||||
|
│ ├── init.ts # Initialize nodes
|
||||||
|
│ ├── start.ts # Start nodes/network
|
||||||
|
│ ├── stop.ts # Stop nodes/network
|
||||||
|
│ ├── status.ts # Check status
|
||||||
|
│ └── config.ts # Configuration management
|
||||||
|
└── lib/ # Utility libraries
|
||||||
|
├── constants.ts # Constants and defaults
|
||||||
|
├── logger.ts # Beautiful logging
|
||||||
|
├── config.ts # SQLite configuration manager
|
||||||
|
├── toml.ts # TOML file utilities
|
||||||
|
├── docker.ts # Docker operations
|
||||||
|
└── node.ts # Node operations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
sonrctl is built with a clean, modular architecture:
|
||||||
|
|
||||||
|
1. **Command Layer** - Handles user input and command routing
|
||||||
|
2. **Library Layer** - Provides utilities for common operations
|
||||||
|
3. **Storage Layer** - SQLite database for configuration persistence
|
||||||
|
|
||||||
|
### Key Design Decisions
|
||||||
|
|
||||||
|
- **Bun Runtime** - For blazingly fast execution and built-in tools
|
||||||
|
- **SQLite Storage** - Lightweight, fast, and portable
|
||||||
|
- **No External Dependencies** - Minimal deps, maximum performance
|
||||||
|
- **Clean Separation** - Commands, libraries, and utilities are separate
|
||||||
|
- **Type Safety** - Full TypeScript support
|
||||||
|
|
||||||
|
## Docker Integration
|
||||||
|
|
||||||
|
sonrctl integrates seamlessly with Docker for testnet deployment:
|
||||||
|
|
||||||
|
- Start/stop docker-compose networks
|
||||||
|
- Manage individual containers
|
||||||
|
- Monitor container status
|
||||||
|
- Execute commands in containers
|
||||||
|
- View container logs
|
||||||
|
|
||||||
|
The CLI can work as a drop-in replacement for manual docker-compose operations.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Bun](https://bun.sh) v1.0+
|
||||||
|
- Docker (for network operations)
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Running Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run directly with Bun
|
||||||
|
bun run src/index.ts help
|
||||||
|
|
||||||
|
# Run a specific command
|
||||||
|
bun run src/index.ts status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Commands
|
||||||
|
|
||||||
|
1. Create a new file in `src/commands/`
|
||||||
|
2. Export an async function that takes `args: string[]`
|
||||||
|
3. Import and add to the switch statement in `src/index.ts`
|
||||||
|
4. Update the help text
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/commands/mycommand.ts
|
||||||
|
export async function mycommand(args: string[]) {
|
||||||
|
Logger.header('My Command');
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/index.ts
|
||||||
|
import { mycommand } from './commands/mycommand';
|
||||||
|
|
||||||
|
// Add to switch statement
|
||||||
|
case 'mycommand':
|
||||||
|
await mycommand(commandArgs);
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Setting Up a Testnet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install the binary
|
||||||
|
sonrctl install
|
||||||
|
|
||||||
|
# 2. Start the testnet network
|
||||||
|
sonrctl start network --detach
|
||||||
|
|
||||||
|
# 3. Check status
|
||||||
|
sonrctl status network
|
||||||
|
|
||||||
|
# 4. View logs
|
||||||
|
docker logs -f val-naruto
|
||||||
|
|
||||||
|
# 5. Stop when done
|
||||||
|
sonrctl stop network
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Custom Node
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Initialize a custom validator
|
||||||
|
sonrctl init validator my-validator \
|
||||||
|
--chain-id mychain-1 \
|
||||||
|
--home ~/.sonr/my-validator \
|
||||||
|
--rpc-port 26667
|
||||||
|
|
||||||
|
# 2. Configure genesis (manual step)
|
||||||
|
# Edit ~/.sonr/my-validator/config/genesis.json
|
||||||
|
|
||||||
|
# 3. Start the node
|
||||||
|
sonrctl start my-validator
|
||||||
|
|
||||||
|
# 4. Check status
|
||||||
|
sonrctl status my-validator
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Command not found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure Bun's bin directory is in your PATH
|
||||||
|
export PATH="$HOME/.bun/bin:$PATH"
|
||||||
|
|
||||||
|
# Re-link the CLI
|
||||||
|
bun link
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker permission denied
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add your user to the docker group
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
|
||||||
|
# Log out and back in, or run:
|
||||||
|
newgrp docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary not found after install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if ~/.local/bin is in your PATH
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
# Add to your shell profile
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests if applicable
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache 2.0 - See LICENSE file for details
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Sonr Network](https://sonr.io)
|
||||||
|
- [Documentation](https://docs.sonr.io)
|
||||||
|
- [GitHub](https://github.com/sonr-io/sonr)
|
||||||
|
- [Bun](https://bun.sh)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Built with ❤️ using Bun**
|
||||||
@@ -349,3 +349,50 @@ func GetEventAttribute(event struct {
|
|||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DoRequest performs a public HTTP GET request (wrapper around private doRequest)
|
||||||
|
// This is used by test helpers to query custom endpoints
|
||||||
|
func (c *StarshipClient) DoRequest(ctx context.Context, url string, target any) error {
|
||||||
|
return c.doRequest(ctx, url, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxResponse represents a transaction response
|
||||||
|
type TxResponse struct {
|
||||||
|
Height string `json:"height"`
|
||||||
|
TxHash string `json:"txhash"`
|
||||||
|
Codespace string `json:"codespace"`
|
||||||
|
Code uint32 `json:"code"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
RawLog string `json:"raw_log"`
|
||||||
|
Logs []struct {
|
||||||
|
MsgIndex int `json:"msg_index"`
|
||||||
|
Log string `json:"log"`
|
||||||
|
Events []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Attributes []struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"events"`
|
||||||
|
} `json:"logs"`
|
||||||
|
Info string `json:"info"`
|
||||||
|
GasWanted string `json:"gas_wanted"`
|
||||||
|
GasUsed string `json:"gas_used"`
|
||||||
|
Tx any `json:"tx"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignAndBroadcastTx signs and broadcasts a transaction message
|
||||||
|
// Note: This is a simplified implementation for E2E tests
|
||||||
|
// In production, use proper keyring and transaction signing
|
||||||
|
func (c *StarshipClient) SignAndBroadcastTx(ctx context.Context, from string, msgs ...sdk.Msg) (*TxResponse, error) {
|
||||||
|
// For E2E tests, we'll use the REST API to broadcast transactions
|
||||||
|
// In a real implementation, you would:
|
||||||
|
// 1. Sign the transaction with the account's private key
|
||||||
|
// 2. Encode the transaction
|
||||||
|
// 3. Broadcast via REST or gRPC
|
||||||
|
|
||||||
|
// This is a placeholder that needs proper implementation based on your keyring setup
|
||||||
|
// For now, return an error indicating this needs to be implemented
|
||||||
|
return nil, fmt.Errorf("SignAndBroadcastTx not yet implemented - use CLI or proper SDK client for transaction signing")
|
||||||
|
}
|
||||||
|
|||||||
+4
-2
@@ -18,8 +18,8 @@ replace (
|
|||||||
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0
|
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0
|
||||||
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
|
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
|
||||||
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.8.1
|
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.8.1
|
||||||
github.com/sonr-io/sonr => ../../
|
|
||||||
github.com/sonr-io/crypto => ../../crypto
|
github.com/sonr-io/crypto => ../../crypto
|
||||||
|
github.com/sonr-io/sonr => ../../
|
||||||
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -333,8 +333,8 @@ require (
|
|||||||
github.com/samber/lo v1.47.0 // indirect
|
github.com/samber/lo v1.47.0 // indirect
|
||||||
github.com/sasha-s/go-deadlock v0.3.5 // indirect
|
github.com/sasha-s/go-deadlock v0.3.5 // indirect
|
||||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||||
github.com/sonr-io/sonr v0.0.0-00010101000000-000000000000 // indirect
|
|
||||||
github.com/sonr-io/crypto v0.0.0-00010101000000-000000000000 // indirect
|
github.com/sonr-io/crypto v0.0.0-00010101000000-000000000000 // indirect
|
||||||
|
github.com/sonr-io/sonr v0.0.0-00010101000000-000000000000 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
@@ -421,3 +421,5 @@ require (
|
|||||||
rsc.io/tmplfunc v0.0.3 // indirect
|
rsc.io/tmplfunc v0.0.3 // indirect
|
||||||
sigs.k8s.io/yaml v1.5.0 // indirect
|
sigs.k8s.io/yaml v1.5.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/lyft/protoc-gen-validate => github.com/envoyproxy/protoc-gen-validate v1.1.0
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
.PHONY: test test-verbose test-quick test-single clean help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: test
|
||||||
|
|
||||||
|
# Run all E2E tests
|
||||||
|
test:
|
||||||
|
@echo "Running USDC Swap E2E tests..."
|
||||||
|
go test -v -timeout 10m
|
||||||
|
|
||||||
|
# Run tests with verbose output and detailed logs
|
||||||
|
test-verbose:
|
||||||
|
@echo "Running tests with verbose output..."
|
||||||
|
go test -v -timeout 10m 2>&1 | tee test.log
|
||||||
|
@echo "Test log saved to test.log"
|
||||||
|
|
||||||
|
# Run quick tests (skip slow integration tests)
|
||||||
|
test-quick:
|
||||||
|
@echo "Running quick tests..."
|
||||||
|
go test -v -short -timeout 5m
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
# Usage: make test-single TEST=Test04_ExecuteUSDCSwap_SNRToUSDC
|
||||||
|
test-single:
|
||||||
|
@echo "Running single test: $(TEST)"
|
||||||
|
go test -v -run "TestUSDCSwapE2E/$(TEST)" -timeout 5m
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
test-coverage:
|
||||||
|
@echo "Running tests with coverage..."
|
||||||
|
go test -v -coverprofile=coverage.out -timeout 10m
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
@echo "Coverage report generated: coverage.html"
|
||||||
|
|
||||||
|
# Run specific test categories
|
||||||
|
test-config:
|
||||||
|
@echo "Running configuration tests..."
|
||||||
|
go test -v -run "Test01" -timeout 2m
|
||||||
|
|
||||||
|
test-did:
|
||||||
|
@echo "Running DID tests..."
|
||||||
|
go test -v -run "Test02" -timeout 3m
|
||||||
|
|
||||||
|
test-dex:
|
||||||
|
@echo "Running DEX account tests..."
|
||||||
|
go test -v -run "Test03" -timeout 3m
|
||||||
|
|
||||||
|
test-swaps:
|
||||||
|
@echo "Running swap execution tests..."
|
||||||
|
go test -v -run "Test04|Test05|Test09" -timeout 10m
|
||||||
|
|
||||||
|
test-validation:
|
||||||
|
@echo "Running validation tests..."
|
||||||
|
go test -v -run "Test06" -timeout 3m
|
||||||
|
|
||||||
|
test-query:
|
||||||
|
@echo "Running query tests..."
|
||||||
|
go test -v -run "Test08" -timeout 2m
|
||||||
|
|
||||||
|
# Verify prerequisites
|
||||||
|
check-prereqs:
|
||||||
|
@echo "Checking prerequisites..."
|
||||||
|
@command -v go >/dev/null 2>&1 || { echo "Go is not installed"; exit 1; }
|
||||||
|
@echo "✓ Go installed: $$(go version)"
|
||||||
|
@command -v snrd >/dev/null 2>&1 || { echo "snrd is not installed"; exit 1; }
|
||||||
|
@echo "✓ snrd installed"
|
||||||
|
@curl -s http://localhost:1317/cosmos/base/tendermint/v1beta1/node_info >/dev/null 2>&1 || \
|
||||||
|
{ echo "✗ Sonr testnet not running at localhost:1317"; exit 1; }
|
||||||
|
@echo "✓ Sonr testnet running"
|
||||||
|
@echo "All prerequisites met!"
|
||||||
|
|
||||||
|
# Setup test environment
|
||||||
|
setup:
|
||||||
|
@echo "Setting up test environment..."
|
||||||
|
@echo "1. Checking network status..."
|
||||||
|
@curl -s http://localhost:1317/cosmos/base/tendermint/v1beta1/node_info | grep -q network && \
|
||||||
|
echo "✓ Network is running" || { echo "✗ Network not accessible"; exit 1; }
|
||||||
|
@echo "2. Checking faucet..."
|
||||||
|
@curl -s http://localhost:8000/health >/dev/null 2>&1 && \
|
||||||
|
echo "✓ Faucet is running" || echo "⚠ Faucet not accessible (tests may fail)"
|
||||||
|
@echo "Setup complete!"
|
||||||
|
|
||||||
|
# Verify IBC connections
|
||||||
|
check-ibc:
|
||||||
|
@echo "Checking IBC connections..."
|
||||||
|
@snrd query ibc connection connections --node tcp://localhost:26657 --output json | \
|
||||||
|
jq -r '.connections[] | "\(.id): \(.state)"'
|
||||||
|
@echo ""
|
||||||
|
@echo "Checking IBC channels..."
|
||||||
|
@snrd query ibc channel channels --node tcp://localhost:26657 --output json | \
|
||||||
|
jq -r '.channels[] | "\(.port_id)/\(.channel_id): \(.state)"'
|
||||||
|
|
||||||
|
# Query DEX module status
|
||||||
|
check-dex:
|
||||||
|
@echo "Checking DEX module..."
|
||||||
|
@snrd query dex params --node tcp://localhost:26657 --output json | \
|
||||||
|
jq -r '.params | "Enabled: \(.enabled), Max Accounts: \(.max_accounts_per_did)"'
|
||||||
|
|
||||||
|
# Clean test artifacts
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning test artifacts..."
|
||||||
|
@rm -f test.log coverage.out coverage.html
|
||||||
|
@echo "✓ Cleaned"
|
||||||
|
|
||||||
|
# Watch mode - run tests on file changes
|
||||||
|
# Requires: entr (brew install entr or apt-get install entr)
|
||||||
|
watch:
|
||||||
|
@echo "Watching for file changes..."
|
||||||
|
@ls *.go | entr -c make test-verbose
|
||||||
|
|
||||||
|
# Benchmark tests
|
||||||
|
bench:
|
||||||
|
@echo "Running benchmarks..."
|
||||||
|
go test -bench=. -benchmem -timeout 10m
|
||||||
|
|
||||||
|
# Generate test data
|
||||||
|
generate-test-data:
|
||||||
|
@echo "Generating test DIDs and accounts..."
|
||||||
|
@for i in {1..5}; do \
|
||||||
|
did="did:snr:test_preset_$$i"; \
|
||||||
|
echo "$$did"; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Help target
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " make test - Run all E2E tests"
|
||||||
|
@echo " make test-verbose - Run with verbose output"
|
||||||
|
@echo " make test-quick - Run quick tests only"
|
||||||
|
@echo " make test-single TEST=<name> - Run a single test"
|
||||||
|
@echo " make test-coverage - Run with coverage report"
|
||||||
|
@echo " make test-config - Run configuration tests"
|
||||||
|
@echo " make test-did - Run DID tests"
|
||||||
|
@echo " make test-dex - Run DEX account tests"
|
||||||
|
@echo " make test-swaps - Run swap execution tests"
|
||||||
|
@echo " make test-validation - Run validation tests"
|
||||||
|
@echo " make test-query - Run query tests"
|
||||||
|
@echo " make check-prereqs - Verify prerequisites"
|
||||||
|
@echo " make setup - Setup test environment"
|
||||||
|
@echo " make check-ibc - Check IBC connections"
|
||||||
|
@echo " make check-dex - Check DEX module status"
|
||||||
|
@echo " make clean - Clean test artifacts"
|
||||||
|
@echo " make watch - Watch mode (requires entr)"
|
||||||
|
@echo " make bench - Run benchmarks"
|
||||||
|
@echo " make help - Show this help"
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
# USDC Swap with DID E2E Tests
|
||||||
|
|
||||||
|
Comprehensive end-to-end tests for USDC swap functionality with DID-based Interchain Accounts (ICA) on Noble testnet.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This test suite validates the complete USDC swap flow including:
|
||||||
|
- DID creation and management
|
||||||
|
- DEX account registration via ICA
|
||||||
|
- Cross-chain USDC swaps (SNR ↔ USDC)
|
||||||
|
- Noble testnet IBC integration
|
||||||
|
- Swap routing and slippage protection
|
||||||
|
- Event verification and transaction history
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Network Setup
|
||||||
|
|
||||||
|
1. **Sonr Testnet**: Running local testnet with DEX module enabled
|
||||||
|
2. **Noble Testnet Connection**: IBC connection established to Noble testnet (grand-1)
|
||||||
|
3. **IBC Relayer**: Active relayer between Sonr and Noble chains
|
||||||
|
4. **Test Tokens**:
|
||||||
|
- SNR tokens for gas and swaps
|
||||||
|
- USDC tokens on Noble testnet (optional for full E2E)
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The tests use these default configurations:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Noble Testnet
|
||||||
|
NobleChainID = "noble-grand-1"
|
||||||
|
NobleConnectionID = "connection-noble" // Update with actual connection
|
||||||
|
NobleUSDCDenom = "uusdc"
|
||||||
|
|
||||||
|
// Test Amounts
|
||||||
|
USDCTestAmount = 1_000_000 // 1 USDC (6 decimals)
|
||||||
|
SNRSwapAmount = 1_000_000 // 1 SNR (6 decimals)
|
||||||
|
SlippageTolerance = 0.05 // 5% slippage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Suite Structure
|
||||||
|
|
||||||
|
### Test Cases
|
||||||
|
|
||||||
|
#### 01. Verify Noble Configuration
|
||||||
|
**Purpose**: Validates Noble chain configuration and constants
|
||||||
|
**Validates**:
|
||||||
|
- Noble chain ID recognition
|
||||||
|
- USDC denomination configuration
|
||||||
|
- Decimal places (6 for USDC)
|
||||||
|
- Chain type detection
|
||||||
|
|
||||||
|
#### 02. Create DID Document
|
||||||
|
**Purpose**: Creates a test DID for swap operations
|
||||||
|
**Operations**:
|
||||||
|
- Generate unique DID for test
|
||||||
|
- Create DID document on-chain
|
||||||
|
- Verify DID can be queried
|
||||||
|
- Validate DID structure
|
||||||
|
|
||||||
|
#### 03. Register DEX Account
|
||||||
|
**Purpose**: Registers an ICA account on Noble connection
|
||||||
|
**Operations**:
|
||||||
|
- Register DEX account with Noble connection
|
||||||
|
- Enable swap and liquidity features
|
||||||
|
- Wait for ICA channel handshake
|
||||||
|
- Verify account status and ICA address
|
||||||
|
|
||||||
|
**Expected States**:
|
||||||
|
- `ACCOUNT_STATUS_PENDING`: Initial state during handshake
|
||||||
|
- `ACCOUNT_STATUS_ACTIVE`: Account ready for operations
|
||||||
|
- `ACCOUNT_STATUS_FAILED`: Handshake failed
|
||||||
|
|
||||||
|
#### 04. Execute USDC Swap (SNR → USDC)
|
||||||
|
**Purpose**: Tests swapping SNR to USDC on Noble
|
||||||
|
**Flow**:
|
||||||
|
1. Setup DID and DEX account
|
||||||
|
2. Calculate minimum output with slippage
|
||||||
|
3. Execute swap via ICA
|
||||||
|
4. Verify transaction success
|
||||||
|
5. Check swap event emission
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
```go
|
||||||
|
Amount: 1_000_000 usnr (1 SNR)
|
||||||
|
MinAmountOut: 950_000 uusdc (0.95 USDC - 5% slippage)
|
||||||
|
Timeout: 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 05. Execute USDC Swap (USDC → SNR)
|
||||||
|
**Purpose**: Tests reverse swap (USDC to SNR)
|
||||||
|
**Flow**: Same as Test 04 but with reversed denoms
|
||||||
|
|
||||||
|
#### 06. Swap With Invalid Parameters
|
||||||
|
**Purpose**: Tests error handling and validation
|
||||||
|
**Test Cases**:
|
||||||
|
- Zero amount swap
|
||||||
|
- Same source/target denom
|
||||||
|
- Negative minimum output
|
||||||
|
- Invalid connection ID
|
||||||
|
|
||||||
|
**Expected**: All cases should fail gracefully
|
||||||
|
|
||||||
|
#### 07. Swap With UCAN Permission
|
||||||
|
**Purpose**: Tests UCAN-authorized delegated swaps
|
||||||
|
**Status**: Currently skipped (requires full UCAN setup)
|
||||||
|
**Features**:
|
||||||
|
- UCAN token generation
|
||||||
|
- Permission validation
|
||||||
|
- Delegated execution
|
||||||
|
|
||||||
|
#### 08. Query DEX History
|
||||||
|
**Purpose**: Verifies transaction history tracking
|
||||||
|
**Validates**:
|
||||||
|
- History query endpoint
|
||||||
|
- Activity records
|
||||||
|
- Transaction metadata
|
||||||
|
- Status tracking
|
||||||
|
|
||||||
|
#### 09. Multiple Sequential Swaps
|
||||||
|
**Purpose**: Tests executing multiple swaps in sequence
|
||||||
|
**Operations**:
|
||||||
|
- Execute 3 swaps sequentially
|
||||||
|
- Verify each transaction succeeds
|
||||||
|
- Check for nonce/sequence handling
|
||||||
|
- Validate no race conditions
|
||||||
|
|
||||||
|
## Running the Tests
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to test directory
|
||||||
|
cd test/e2e/usdc-swap-did
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
go test -v
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
go test -v -run TestUSDCSwapE2E/Test04_ExecuteUSDCSwap_SNRToUSDC
|
||||||
|
|
||||||
|
# Run with timeout
|
||||||
|
go test -v -timeout 10m
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerequisites Check
|
||||||
|
|
||||||
|
Before running tests, ensure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Verify Noble connection exists
|
||||||
|
snrd query ibc connection end connection-noble
|
||||||
|
|
||||||
|
# 2. Check connection is open
|
||||||
|
# Expected output: state: STATE_OPEN
|
||||||
|
|
||||||
|
# 3. Verify ICA channel
|
||||||
|
snrd query ibc channel channels --node tcp://localhost:26657
|
||||||
|
|
||||||
|
# 4. Check DEX module is enabled
|
||||||
|
snrd query dex params
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full E2E Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Start Sonr testnet
|
||||||
|
make testnet
|
||||||
|
|
||||||
|
# 2. Establish Noble connection (one-time setup)
|
||||||
|
# This requires:
|
||||||
|
# - Noble testnet access
|
||||||
|
# - IBC relayer configuration
|
||||||
|
# - Channel creation
|
||||||
|
|
||||||
|
# 3. Fund test accounts
|
||||||
|
# The tests will auto-fund via faucet, but you can pre-fund:
|
||||||
|
snrd tx bank send validator <test-address> 100000000usnr --yes
|
||||||
|
|
||||||
|
# 4. Run tests
|
||||||
|
cd test/e2e/usdc-swap-did && go test -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Utilities
|
||||||
|
|
||||||
|
### Helper Functions
|
||||||
|
|
||||||
|
#### DID Operations
|
||||||
|
```go
|
||||||
|
// Create test DID
|
||||||
|
did, err := CreateTestDID(ctx, cfg, account, didID)
|
||||||
|
|
||||||
|
// Query DID
|
||||||
|
did, err := QueryDID(ctx, cfg, didID)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DEX Operations
|
||||||
|
```go
|
||||||
|
// Query DEX account
|
||||||
|
account, err := QueryDEXAccount(ctx, cfg, didID, connectionID)
|
||||||
|
|
||||||
|
// Query transaction history
|
||||||
|
history, err := QueryDEXHistory(ctx, cfg, didID)
|
||||||
|
|
||||||
|
// Wait for account activation
|
||||||
|
active, err := WaitForDEXAccountActivation(ctx, cfg, didID, connectionID, timeout)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Swap Utilities
|
||||||
|
```go
|
||||||
|
// Validate parameters
|
||||||
|
err := ValidateSwapParameters(sourceDenom, targetDenom, amount, minOut)
|
||||||
|
|
||||||
|
// Calculate slippage
|
||||||
|
minOut := CalculateMinimumOutput(amount, slippagePct)
|
||||||
|
|
||||||
|
// Format amounts for display
|
||||||
|
formatted := FormatUSDCAmount(1000000) // "1.000000 USDC"
|
||||||
|
formatted = FormatSNRAmount(1000000) // "1.000000 SNR"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
The test suite uses `testify/suite` and `testify/require` for assertions:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Transaction success
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(0), txResp.Code)
|
||||||
|
|
||||||
|
// Balance checks
|
||||||
|
balance := s.getBalance(address, denom)
|
||||||
|
require.True(t, balance.GT(minAmount))
|
||||||
|
|
||||||
|
// Event verification
|
||||||
|
s.verifySwapEvent(txHash, did, connectionID)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Update Connection ID
|
||||||
|
|
||||||
|
Before running tests, update the Noble connection ID in `usdc_swap_test.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
NobleConnectionID = "connection-0" // Replace with actual connection ID
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
To find the connection ID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all connections
|
||||||
|
snrd query ibc connection connections
|
||||||
|
|
||||||
|
# Look for connection to Noble (grand-1)
|
||||||
|
# Update NobleConnectionID constant with the correct value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Test Configuration
|
||||||
|
|
||||||
|
Create a custom config for your environment:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg := &utils.TestConfig{
|
||||||
|
ChainID: "sonrtest_1-1",
|
||||||
|
BaseURL: "http://localhost:1317",
|
||||||
|
NobleConnection: "connection-0", // Your Noble connection
|
||||||
|
// ... other settings
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### 1. Connection Not Found
|
||||||
|
```
|
||||||
|
Error: connection connection-noble not found
|
||||||
|
```
|
||||||
|
**Solution**: Update `NobleConnectionID` constant with actual connection ID
|
||||||
|
|
||||||
|
#### 2. ICA Account Pending
|
||||||
|
```
|
||||||
|
⚠ ICA address pending (channel handshake may be in progress)
|
||||||
|
```
|
||||||
|
**Solution**: Wait for ICA channel handshake to complete (can take 1-2 minutes)
|
||||||
|
|
||||||
|
#### 3. Swap Transaction Fails
|
||||||
|
```
|
||||||
|
Error: DEX account not active
|
||||||
|
```
|
||||||
|
**Solution**: Ensure ICA account is in `ACCOUNT_STATUS_ACTIVE` state
|
||||||
|
|
||||||
|
#### 4. Insufficient Funds
|
||||||
|
```
|
||||||
|
Error: insufficient funds
|
||||||
|
```
|
||||||
|
**Solution**: Fund test account with SNR tokens
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable verbose logging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with verbose output
|
||||||
|
go test -v -run TestUSDCSwapE2E 2>&1 | tee test.log
|
||||||
|
|
||||||
|
# Check specific test output
|
||||||
|
grep "Test 04" test.log
|
||||||
|
|
||||||
|
# View all swap events
|
||||||
|
grep "Swap" test.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify IBC Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check IBC clients
|
||||||
|
snrd query ibc client states
|
||||||
|
|
||||||
|
# Check channels
|
||||||
|
snrd query ibc channel channels
|
||||||
|
|
||||||
|
# Check connections
|
||||||
|
snrd query ibc connection connections
|
||||||
|
|
||||||
|
# Monitor relayer
|
||||||
|
# (depends on your relayer setup)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with CI/CD
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: USDC Swap E2E Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e-usdc-swap:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Start Testnet
|
||||||
|
run: make testnet
|
||||||
|
|
||||||
|
- name: Wait for Network
|
||||||
|
run: sleep 30
|
||||||
|
|
||||||
|
- name: Run E2E Tests
|
||||||
|
run: |
|
||||||
|
cd test/e2e/usdc-swap-did
|
||||||
|
go test -v -timeout 10m
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
### Successful Test Run
|
||||||
|
|
||||||
|
```
|
||||||
|
=== RUN TestUSDCSwapE2E
|
||||||
|
=== RUN TestUSDCSwapE2E/Test01_VerifyNobleConfiguration
|
||||||
|
=== Test 01: Verify Noble Configuration ===
|
||||||
|
✓ Noble configuration verified successfully
|
||||||
|
=== RUN TestUSDCSwapE2E/Test02_CreateDIDDocument
|
||||||
|
=== Test 02: Create DID Document ===
|
||||||
|
✓ DID document created: did:snr:test_1234567890
|
||||||
|
✓ DID query verification successful
|
||||||
|
=== RUN TestUSDCSwapE2E/Test03_RegisterDEXAccount
|
||||||
|
=== Test 03: Register DEX Account ===
|
||||||
|
✓ DEX account registered, tx hash: ABC123...
|
||||||
|
✓ DEX account status: ACCOUNT_STATUS_ACTIVE
|
||||||
|
✓ ICA port ID: icacontroller-did:snr:test_1234567890-connection-0
|
||||||
|
✓ ICA address: noble1abc...xyz
|
||||||
|
=== RUN TestUSDCSwapE2E/Test04_ExecuteUSDCSwap_SNRToUSDC
|
||||||
|
=== Test 04: Execute USDC Swap (SNR → USDC) ===
|
||||||
|
Initial SNR balance: 10000000
|
||||||
|
✓ Swap transaction broadcasted, tx hash: DEF456...
|
||||||
|
✓ Found swap event for DID: did:snr:test_1234567890
|
||||||
|
✓ Swap execution test completed
|
||||||
|
...
|
||||||
|
--- PASS: TestUSDCSwapE2E (45.23s)
|
||||||
|
PASS
|
||||||
|
ok github.com/sonr-io/sonr/test/e2e/usdc-swap-did 45.234s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
1. **Multi-hop Swaps**: Route through multiple pools/chains
|
||||||
|
2. **Liquidity Provision**: Add/remove liquidity tests
|
||||||
|
3. **Limit Orders**: Create and execute limit orders
|
||||||
|
4. **UCAN Integration**: Full delegation testing
|
||||||
|
5. **Performance Tests**: Load testing and benchmarks
|
||||||
|
6. **Failure Recovery**: Test ICA timeout handling
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
When adding new tests:
|
||||||
|
1. Follow existing naming convention (Test##_Description)
|
||||||
|
2. Add helper functions to `helpers.go`
|
||||||
|
3. Update this README with new test documentation
|
||||||
|
4. Ensure tests are idempotent
|
||||||
|
5. Add appropriate logging for debugging
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Noble Documentation](https://docs.noble.xyz)
|
||||||
|
- [IBC Protocol](https://ibc.cosmos.network)
|
||||||
|
- [Sonr DEX Module](../../../../x/dex/README.md)
|
||||||
|
- [DID Module](../../../../x/did/README.md)
|
||||||
|
- [E2E Testing Framework](../README.md)
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
# USDC Swap E2E Test Configuration
|
||||||
|
# Copy this file to config.yaml and customize for your environment
|
||||||
|
|
||||||
|
# Sonr Testnet Configuration
|
||||||
|
sonr:
|
||||||
|
chain_id: "sonrtest_1-1"
|
||||||
|
rpc_endpoint: "http://localhost:26657"
|
||||||
|
rest_endpoint: "http://localhost:1317"
|
||||||
|
grpc_endpoint: "localhost:9090"
|
||||||
|
faucet_endpoint: "http://localhost:8000"
|
||||||
|
|
||||||
|
# Native token configuration
|
||||||
|
staking_denom: "usnr"
|
||||||
|
display_denom: "snr"
|
||||||
|
decimals: 6
|
||||||
|
|
||||||
|
# Test account configuration
|
||||||
|
test_accounts:
|
||||||
|
- name: "test_user_1"
|
||||||
|
mnemonic: "" # Optional: Provide existing mnemonic
|
||||||
|
initial_balance: "100000000usnr" # 100 SNR
|
||||||
|
|
||||||
|
- name: "test_user_2"
|
||||||
|
mnemonic: ""
|
||||||
|
initial_balance: "50000000usnr" # 50 SNR
|
||||||
|
|
||||||
|
# Noble Testnet Configuration
|
||||||
|
noble:
|
||||||
|
chain_id: "noble-grand-1"
|
||||||
|
rpc_endpoint: "https://noble-testnet-rpc.polkachu.com:443"
|
||||||
|
rest_endpoint: "https://noble-testnet-api.polkachu.com"
|
||||||
|
grpc_endpoint: "noble-testnet-grpc.polkachu.com:21590"
|
||||||
|
|
||||||
|
# USDC configuration
|
||||||
|
usdc_denom: "uusdc"
|
||||||
|
usdc_decimals: 6
|
||||||
|
|
||||||
|
# Circle attestation (for USDC)
|
||||||
|
attestation:
|
||||||
|
enabled: true
|
||||||
|
domain: 4 # Noble testnet domain
|
||||||
|
|
||||||
|
# IBC Configuration
|
||||||
|
ibc:
|
||||||
|
# Connection from Sonr to Noble
|
||||||
|
# Update this with your actual connection ID
|
||||||
|
noble_connection_id: "connection-0"
|
||||||
|
|
||||||
|
# IBC transfer channel
|
||||||
|
# Update with actual channel after connection
|
||||||
|
transfer_channel_id: "channel-0"
|
||||||
|
|
||||||
|
# Relayer configuration (optional)
|
||||||
|
relayer:
|
||||||
|
enabled: false
|
||||||
|
type: "hermes" # or "rly"
|
||||||
|
path: "sonr-noble"
|
||||||
|
|
||||||
|
# DEX Module Configuration
|
||||||
|
dex:
|
||||||
|
# Features to enable for test accounts
|
||||||
|
default_features:
|
||||||
|
- "swap"
|
||||||
|
- "liquidity"
|
||||||
|
- "orders"
|
||||||
|
|
||||||
|
# ICA configuration
|
||||||
|
ica:
|
||||||
|
# Timeout for ICA operations
|
||||||
|
timeout: "300s"
|
||||||
|
|
||||||
|
# Retry configuration
|
||||||
|
max_retries: 3
|
||||||
|
retry_delay: "5s"
|
||||||
|
|
||||||
|
# Account creation
|
||||||
|
auto_register: true
|
||||||
|
|
||||||
|
# Swap Test Configuration
|
||||||
|
swap:
|
||||||
|
# Test amounts
|
||||||
|
amounts:
|
||||||
|
snr_to_usdc: "1000000" # 1 SNR
|
||||||
|
usdc_to_snr: "5000000" # 5 USDC
|
||||||
|
small_swap: "100000" # 0.1 tokens
|
||||||
|
large_swap: "50000000" # 50 tokens
|
||||||
|
|
||||||
|
# Slippage configuration
|
||||||
|
slippage:
|
||||||
|
default_percent: 5.0 # 5% default slippage
|
||||||
|
max_percent: 10.0 # 10% maximum allowed
|
||||||
|
min_percent: 0.1 # 0.1% minimum
|
||||||
|
|
||||||
|
# Timeout configuration
|
||||||
|
timeouts:
|
||||||
|
swap_tx: "60s" # Individual swap timeout
|
||||||
|
multi_swap: "300s" # Multiple swaps timeout
|
||||||
|
history_query: "30s" # History query timeout
|
||||||
|
|
||||||
|
# Routing configuration
|
||||||
|
routing:
|
||||||
|
prefer_direct: true # Prefer direct swaps when available
|
||||||
|
max_hops: 3 # Maximum routing hops
|
||||||
|
usdc_intermediary: true # Use USDC as intermediary
|
||||||
|
|
||||||
|
# Test Configuration
|
||||||
|
test:
|
||||||
|
# Parallel execution
|
||||||
|
parallel: false
|
||||||
|
|
||||||
|
# Timeout configuration
|
||||||
|
timeout:
|
||||||
|
default: "10m"
|
||||||
|
extended: "30m"
|
||||||
|
quick: "5m"
|
||||||
|
|
||||||
|
# Retry configuration
|
||||||
|
retry:
|
||||||
|
enabled: true
|
||||||
|
max_attempts: 3
|
||||||
|
delay: "5s"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging:
|
||||||
|
level: "info" # debug, info, warn, error
|
||||||
|
verbose: true
|
||||||
|
save_to_file: true
|
||||||
|
file_path: "test.log"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
cleanup:
|
||||||
|
# Clean up test accounts after tests
|
||||||
|
accounts: true
|
||||||
|
|
||||||
|
# Clean up test DIDs after tests
|
||||||
|
dids: false # Keep for debugging
|
||||||
|
|
||||||
|
# Clean up test data
|
||||||
|
artifacts: true
|
||||||
|
|
||||||
|
# DID Configuration
|
||||||
|
did:
|
||||||
|
# DID prefix
|
||||||
|
prefix: "did:snr:test"
|
||||||
|
|
||||||
|
# Generate unique DIDs per test
|
||||||
|
unique_per_test: true
|
||||||
|
|
||||||
|
# DID document configuration
|
||||||
|
document:
|
||||||
|
verification_methods:
|
||||||
|
- type: "EcdsaSecp256k1VerificationKey2019"
|
||||||
|
purpose: ["authentication", "assertionMethod"]
|
||||||
|
|
||||||
|
services:
|
||||||
|
- type: "DEXSwapService"
|
||||||
|
endpoint: "https://dex.sonr.io"
|
||||||
|
|
||||||
|
# Monitoring and Alerts
|
||||||
|
monitoring:
|
||||||
|
# Enable transaction monitoring
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Check intervals
|
||||||
|
intervals:
|
||||||
|
balance: "5s"
|
||||||
|
status: "10s"
|
||||||
|
events: "3s"
|
||||||
|
|
||||||
|
# Alerts
|
||||||
|
alerts:
|
||||||
|
# Alert on test failure
|
||||||
|
on_failure: true
|
||||||
|
|
||||||
|
# Alert on slow tests
|
||||||
|
on_slow_test:
|
||||||
|
enabled: true
|
||||||
|
threshold: "5m"
|
||||||
|
|
||||||
|
# Alert on IBC timeout
|
||||||
|
on_ibc_timeout: true
|
||||||
|
|
||||||
|
# Performance Configuration
|
||||||
|
performance:
|
||||||
|
# Concurrent swaps limit
|
||||||
|
max_concurrent_swaps: 10
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
rate_limit:
|
||||||
|
enabled: true
|
||||||
|
requests_per_second: 5
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
resources:
|
||||||
|
max_memory: "1GB"
|
||||||
|
max_goroutines: 100
|
||||||
|
|
||||||
|
# Network Simulation
|
||||||
|
simulation:
|
||||||
|
# Simulate network conditions
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Latency simulation
|
||||||
|
latency:
|
||||||
|
min: "10ms"
|
||||||
|
max: "500ms"
|
||||||
|
|
||||||
|
# Packet loss simulation
|
||||||
|
packet_loss:
|
||||||
|
rate: 0.01 # 1% packet loss
|
||||||
|
|
||||||
|
# Bandwidth limits
|
||||||
|
bandwidth:
|
||||||
|
upload: "100Mbps"
|
||||||
|
download: "100Mbps"
|
||||||
|
|
||||||
|
# Debug Configuration
|
||||||
|
debug:
|
||||||
|
# Enable debug mode
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Save transaction details
|
||||||
|
save_transactions: true
|
||||||
|
transactions_dir: "./transactions"
|
||||||
|
|
||||||
|
# Save state snapshots
|
||||||
|
save_snapshots: true
|
||||||
|
snapshots_dir: "./snapshots"
|
||||||
|
|
||||||
|
# Breakpoints
|
||||||
|
breakpoints:
|
||||||
|
before_swap: false
|
||||||
|
after_swap: false
|
||||||
|
on_error: true
|
||||||
|
|
||||||
|
# Profiling
|
||||||
|
profiling:
|
||||||
|
enabled: false
|
||||||
|
cpu: true
|
||||||
|
memory: true
|
||||||
|
output_dir: "./profiles"
|
||||||
|
|
||||||
|
# CI/CD Configuration
|
||||||
|
ci:
|
||||||
|
# GitHub Actions
|
||||||
|
github_actions:
|
||||||
|
enabled: true
|
||||||
|
secrets:
|
||||||
|
noble_rpc: "NOBLE_RPC_ENDPOINT"
|
||||||
|
test_mnemonic: "TEST_ACCOUNT_MNEMONIC"
|
||||||
|
|
||||||
|
# Report generation
|
||||||
|
reports:
|
||||||
|
format: "junit" # junit, json, html
|
||||||
|
output_dir: "./test-reports"
|
||||||
|
|
||||||
|
# Artifacts
|
||||||
|
artifacts:
|
||||||
|
upload: true
|
||||||
|
retention_days: 30
|
||||||
|
|
||||||
|
# Environment-specific overrides
|
||||||
|
environments:
|
||||||
|
# Local development
|
||||||
|
local:
|
||||||
|
sonr:
|
||||||
|
rest_endpoint: "http://localhost:1317"
|
||||||
|
noble:
|
||||||
|
# Use local Noble node if available
|
||||||
|
rest_endpoint: "http://localhost:1318"
|
||||||
|
test:
|
||||||
|
timeout:
|
||||||
|
default: "5m"
|
||||||
|
|
||||||
|
# Staging environment
|
||||||
|
staging:
|
||||||
|
sonr:
|
||||||
|
rest_endpoint: "https://staging-api.sonr.io"
|
||||||
|
noble:
|
||||||
|
rest_endpoint: "https://noble-testnet-api.polkachu.com"
|
||||||
|
test:
|
||||||
|
timeout:
|
||||||
|
default: "15m"
|
||||||
|
|
||||||
|
# Production testing
|
||||||
|
production:
|
||||||
|
sonr:
|
||||||
|
rest_endpoint: "https://api.sonr.io"
|
||||||
|
noble:
|
||||||
|
rest_endpoint: "https://noble-api.polkachu.com"
|
||||||
|
test:
|
||||||
|
timeout:
|
||||||
|
default: "30m"
|
||||||
|
retry:
|
||||||
|
max_attempts: 5
|
||||||
|
|
||||||
|
# Notes and Documentation Links
|
||||||
|
documentation:
|
||||||
|
# Useful links
|
||||||
|
links:
|
||||||
|
noble_docs: "https://docs.noble.xyz"
|
||||||
|
sonr_docs: "https://docs.sonr.io"
|
||||||
|
ibc_docs: "https://ibc.cosmos.network"
|
||||||
|
dex_module: "https://github.com/sonr-io/sonr/tree/master/x/dex"
|
||||||
|
|
||||||
|
# Known issues
|
||||||
|
known_issues:
|
||||||
|
- "ICA channel handshake can take 1-2 minutes"
|
||||||
|
- "First swap may timeout if pools are not initialized"
|
||||||
|
- "USDC attestation required for Noble testnet"
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
troubleshooting:
|
||||||
|
connection_failed: "Verify IBC connection is established: snrd query ibc connection end <connection-id>"
|
||||||
|
account_pending: "Wait for ICA channel handshake to complete (1-2 minutes)"
|
||||||
|
insufficient_funds: "Fund test account: snrd tx bank send validator <address> 100000000usnr"
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
package usdcswapdid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sonr-io/sonr/test/e2e/utils"
|
||||||
|
dextypes "github.com/sonr-io/sonr/x/dex/types"
|
||||||
|
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateTestDID creates a DID document for testing
|
||||||
|
// Returns the created DID document or error
|
||||||
|
// Note: For E2E tests, DIDs should be created using CLI commands before running tests
|
||||||
|
// This function checks if the DID exists and returns it, or returns instructions to create it
|
||||||
|
func CreateTestDID(ctx context.Context, cfg *utils.TestConfig, account, didID string) (*didtypes.DIDDocument, error) {
|
||||||
|
// Check if DID already exists
|
||||||
|
existing, err := QueryDID(ctx, cfg, didID)
|
||||||
|
if err == nil && existing != nil {
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DID doesn't exist - return error with instructions
|
||||||
|
return nil, fmt.Errorf("DID %s not found. Create it using CLI:\n"+
|
||||||
|
" snrd tx did create-did %s --from %s --chain-id %s --yes",
|
||||||
|
didID, didID, account, cfg.ChainID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryDID queries a DID document by ID
|
||||||
|
func QueryDID(ctx context.Context, cfg *utils.TestConfig, didID string) (*didtypes.DIDDocument, error) {
|
||||||
|
// Build query URL
|
||||||
|
url := fmt.Sprintf("%s/sonr/did/v1/did/%s", cfg.BaseURL, didID)
|
||||||
|
|
||||||
|
// Response structure
|
||||||
|
var response struct {
|
||||||
|
DidDocument *didtypes.DIDDocument `json:"did_document"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make HTTP request
|
||||||
|
if err := cfg.Client.DoRequest(ctx, url, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query DID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.DidDocument, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryDEXAccount queries a DEX account by DID and connection ID
|
||||||
|
func QueryDEXAccount(ctx context.Context, cfg *utils.TestConfig, didID, connectionID string) (*dextypes.InterchainDEXAccount, error) {
|
||||||
|
// Build query URL
|
||||||
|
url := fmt.Sprintf("%s/sonr/dex/v1/account/%s/%s", cfg.BaseURL, didID, connectionID)
|
||||||
|
|
||||||
|
// Response structure
|
||||||
|
var response struct {
|
||||||
|
Account *dextypes.InterchainDEXAccount `json:"account"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make HTTP request
|
||||||
|
if err := cfg.Client.DoRequest(ctx, url, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query DEX account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryDEXHistory queries transaction history for a DID
|
||||||
|
func QueryDEXHistory(ctx context.Context, cfg *utils.TestConfig, didID string) ([]*dextypes.DEXActivity, error) {
|
||||||
|
// Build query URL
|
||||||
|
url := fmt.Sprintf("%s/sonr/dex/v1/history/%s", cfg.BaseURL, didID)
|
||||||
|
|
||||||
|
// Response structure
|
||||||
|
var response struct {
|
||||||
|
History []*dextypes.DEXActivity `json:"history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make HTTP request
|
||||||
|
if err := cfg.Client.DoRequest(ctx, url, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query DEX history: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.History, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryAllDEXAccounts queries all DEX accounts
|
||||||
|
func QueryAllDEXAccounts(ctx context.Context, cfg *utils.TestConfig) ([]*dextypes.InterchainDEXAccount, error) {
|
||||||
|
// Build query URL
|
||||||
|
url := fmt.Sprintf("%s/sonr/dex/v1/accounts", cfg.BaseURL)
|
||||||
|
|
||||||
|
// Response structure
|
||||||
|
var response struct {
|
||||||
|
Accounts []*dextypes.InterchainDEXAccount `json:"accounts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make HTTP request
|
||||||
|
if err := cfg.Client.DoRequest(ctx, url, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query all DEX accounts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForDEXAccountActivation waits for a DEX account to become active
|
||||||
|
// Returns true if account is active, false if timeout
|
||||||
|
func WaitForDEXAccountActivation(ctx context.Context, cfg *utils.TestConfig, didID, connectionID string, timeout time.Duration) (bool, error) {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
account, err := QueryDEXAccount(ctx, cfg, didID, connectionID)
|
||||||
|
if err != nil {
|
||||||
|
// Account not found yet, continue waiting
|
||||||
|
time.Sleep(cfg.BlockTime)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Status == dextypes.ACCOUNT_STATUS_ACTIVE {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for failed status
|
||||||
|
if account.Status == dextypes.ACCOUNT_STATUS_FAILED {
|
||||||
|
return false, fmt.Errorf("DEX account activation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(cfg.BlockTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("timeout waiting for account activation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyICAAccountAddress verifies that an ICA account has been created
|
||||||
|
func VerifyICAAccountAddress(ctx context.Context, cfg *utils.TestConfig, didID, connectionID string) (string, error) {
|
||||||
|
account, err := QueryDEXAccount(ctx, cfg, didID, connectionID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to query DEX account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.AccountAddress == "" {
|
||||||
|
return "", fmt.Errorf("ICA account address not yet assigned")
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.AccountAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSwapParameters validates swap parameters before execution
|
||||||
|
func ValidateSwapParameters(sourceDenom, targetDenom string, amount, minOut int64) error {
|
||||||
|
if sourceDenom == "" {
|
||||||
|
return fmt.Errorf("source denom cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetDenom == "" {
|
||||||
|
return fmt.Errorf("target denom cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sourceDenom == targetDenom {
|
||||||
|
return fmt.Errorf("source and target denoms must be different")
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount <= 0 {
|
||||||
|
return fmt.Errorf("swap amount must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if minOut < 0 {
|
||||||
|
return fmt.Errorf("min output cannot be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if minOut > amount {
|
||||||
|
return fmt.Errorf("min output cannot exceed input amount (unless exchange rate > 1)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateMinimumOutput calculates minimum output based on slippage tolerance
|
||||||
|
// slippagePct should be between 0 and 100 (e.g., 5 for 5% slippage)
|
||||||
|
func CalculateMinimumOutput(inputAmount int64, slippagePct float64) int64 {
|
||||||
|
if slippagePct < 0 {
|
||||||
|
slippagePct = 0
|
||||||
|
}
|
||||||
|
if slippagePct > 100 {
|
||||||
|
slippagePct = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplier := (100 - slippagePct) / 100
|
||||||
|
return int64(float64(inputAmount) * multiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatUSDCAmount formats a USDC amount for display
|
||||||
|
// USDC has 6 decimals, so 1000000 = 1.000000 USDC
|
||||||
|
func FormatUSDCAmount(amount int64) string {
|
||||||
|
whole := amount / 1_000_000
|
||||||
|
fractional := amount % 1_000_000
|
||||||
|
return fmt.Sprintf("%d.%06d USDC", whole, fractional)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatSNRAmount formats a SNR amount for display
|
||||||
|
// SNR has 6 decimals (usnr), so 1000000 = 1.000000 SNR
|
||||||
|
func FormatSNRAmount(amount int64) string {
|
||||||
|
whole := amount / 1_000_000
|
||||||
|
fractional := amount % 1_000_000
|
||||||
|
return fmt.Sprintf("%d.%06d SNR", whole, fractional)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseIBCDenom parses an IBC denom to extract the base denom
|
||||||
|
// IBC denoms have format: ibc/HASH
|
||||||
|
func ParseIBCDenom(ibcDenom string) (hash string, isIBC bool) {
|
||||||
|
if len(ibcDenom) > 4 && ibcDenom[:4] == "ibc/" {
|
||||||
|
return ibcDenom[4:], true
|
||||||
|
}
|
||||||
|
return ibcDenom, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildNobleIBCDenom builds the IBC denom for Noble USDC on Sonr chain
|
||||||
|
// This requires knowing the IBC channel from Noble to Sonr
|
||||||
|
func BuildNobleIBCDenom(channelID string) string {
|
||||||
|
// In actual implementation, this would use IBC denom trace hashing
|
||||||
|
// For now, return a placeholder
|
||||||
|
return fmt.Sprintf("ibc/noble_usdc_via_%s", channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractSwapEventData extracts relevant data from a swap event
|
||||||
|
type SwapEventData struct {
|
||||||
|
DID string
|
||||||
|
ConnectionID string
|
||||||
|
SourceDenom string
|
||||||
|
TargetDenom string
|
||||||
|
Amount string
|
||||||
|
MinAmountOut string
|
||||||
|
Sequence string
|
||||||
|
SwapType string
|
||||||
|
ICAAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSwapEvent parses a swap event from transaction logs
|
||||||
|
func ParseSwapEvent(events []interface{}) (*SwapEventData, error) {
|
||||||
|
// This would parse the actual event structure from the transaction
|
||||||
|
// For now, return a placeholder
|
||||||
|
return &SwapEventData{}, fmt.Errorf("event parsing not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyNobleConnection verifies that a Noble IBC connection exists
|
||||||
|
func VerifyNobleConnection(ctx context.Context, cfg *utils.TestConfig, connectionID string) error {
|
||||||
|
// Build query URL for connection
|
||||||
|
url := fmt.Sprintf("%s/ibc/core/connection/v1/connections/%s", cfg.BaseURL, connectionID)
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Connection struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
} `json:"connection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Client.DoRequest(ctx, url, &response); err != nil {
|
||||||
|
return fmt.Errorf("failed to query connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Connection.State != "STATE_OPEN" {
|
||||||
|
return fmt.Errorf("connection is not open: %s", response.Connection.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Executable
+278
@@ -0,0 +1,278 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# USDC Swap E2E Test Setup Script
|
||||||
|
# This script helps set up the environment for USDC swap E2E tests
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SONR_RPC="${SONR_RPC:-http://localhost:26657}"
|
||||||
|
SONR_REST="${SONR_REST:-http://localhost:1317}"
|
||||||
|
FAUCET_URL="${FAUCET_URL:-http://localhost:8000}"
|
||||||
|
NOBLE_CONNECTION="${NOBLE_CONNECTION:-connection-0}"
|
||||||
|
|
||||||
|
echo -e "${BLUE}=== USDC Swap E2E Test Setup ===${NC}\n"
|
||||||
|
|
||||||
|
# Function to print status
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}✓${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}✗${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if command exists
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: Check prerequisites
|
||||||
|
echo -e "${BLUE}Step 1: Checking prerequisites...${NC}"
|
||||||
|
|
||||||
|
if ! command_exists go; then
|
||||||
|
print_error "Go is not installed. Please install Go 1.21 or later."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_status "Go installed: $(go version | awk '{print $3}')"
|
||||||
|
|
||||||
|
if ! command_exists snrd; then
|
||||||
|
print_error "snrd is not installed. Please install Sonr daemon."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_status "snrd installed"
|
||||||
|
|
||||||
|
if ! command_exists jq; then
|
||||||
|
print_warning "jq is not installed. Some features may not work. Install with: brew install jq"
|
||||||
|
else
|
||||||
|
print_status "jq installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command_exists curl; then
|
||||||
|
print_error "curl is not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_status "curl installed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 2: Check network status
|
||||||
|
echo -e "${BLUE}Step 2: Checking network status...${NC}"
|
||||||
|
|
||||||
|
if curl -s "${SONR_REST}/cosmos/base/tendermint/v1beta1/node_info" >/dev/null 2>&1; then
|
||||||
|
CHAIN_ID=$(curl -s "${SONR_REST}/cosmos/base/tendermint/v1beta1/node_info" | jq -r '.default_node_info.network')
|
||||||
|
print_status "Sonr testnet is running (Chain ID: $CHAIN_ID)"
|
||||||
|
else
|
||||||
|
print_error "Sonr testnet is not accessible at ${SONR_REST}"
|
||||||
|
echo " Start testnet with: make testnet"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s "${FAUCET_URL}/health" >/dev/null 2>&1; then
|
||||||
|
print_status "Faucet is running"
|
||||||
|
else
|
||||||
|
print_warning "Faucet is not accessible. Tests may fail without funded accounts."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 3: Check IBC connections
|
||||||
|
echo -e "${BLUE}Step 3: Checking IBC connections...${NC}"
|
||||||
|
|
||||||
|
CONNECTIONS=$(snrd query ibc connection connections --node ${SONR_RPC} --output json 2>/dev/null | jq -r '.connections // [] | length')
|
||||||
|
|
||||||
|
if [ "$CONNECTIONS" -eq 0 ]; then
|
||||||
|
print_warning "No IBC connections found"
|
||||||
|
echo " You need to establish a connection to Noble testnet"
|
||||||
|
echo " This typically requires:"
|
||||||
|
echo " 1. Setting up an IBC relayer"
|
||||||
|
echo " 2. Creating a client on each chain"
|
||||||
|
echo " 3. Creating a connection"
|
||||||
|
echo ""
|
||||||
|
echo " For testing without Noble, some tests will be skipped."
|
||||||
|
else
|
||||||
|
print_status "Found $CONNECTIONS IBC connection(s)"
|
||||||
|
|
||||||
|
# Check for Noble connection
|
||||||
|
if snrd query ibc connection end "$NOBLE_CONNECTION" --node ${SONR_RPC} --output json 2>/dev/null | jq -e '.connection.state == "STATE_OPEN"' >/dev/null; then
|
||||||
|
print_status "Noble connection ($NOBLE_CONNECTION) is OPEN"
|
||||||
|
else
|
||||||
|
print_warning "Noble connection ($NOBLE_CONNECTION) not found or not open"
|
||||||
|
echo " Update NOBLE_CONNECTION in config or create the connection"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 4: Check IBC channels
|
||||||
|
echo -e "${BLUE}Step 4: Checking IBC channels...${NC}"
|
||||||
|
|
||||||
|
CHANNELS=$(snrd query ibc channel channels --node ${SONR_RPC} --output json 2>/dev/null | jq -r '.channels // [] | length')
|
||||||
|
|
||||||
|
if [ "$CHANNELS" -eq 0 ]; then
|
||||||
|
print_warning "No IBC channels found"
|
||||||
|
else
|
||||||
|
print_status "Found $CHANNELS IBC channel(s)"
|
||||||
|
|
||||||
|
# List channels
|
||||||
|
if command_exists jq; then
|
||||||
|
echo " Channels:"
|
||||||
|
snrd query ibc channel channels --node ${SONR_RPC} --output json 2>/dev/null | \
|
||||||
|
jq -r '.channels[] | " \(.port_id)/\(.channel_id): \(.state)"'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 5: Check DEX module
|
||||||
|
echo -e "${BLUE}Step 5: Checking DEX module...${NC}"
|
||||||
|
|
||||||
|
if snrd query dex params --node ${SONR_RPC} --output json 2>/dev/null | jq -e '.params.enabled == true' >/dev/null; then
|
||||||
|
print_status "DEX module is enabled"
|
||||||
|
|
||||||
|
if command_exists jq; then
|
||||||
|
MAX_ACCOUNTS=$(snrd query dex params --node ${SONR_RPC} --output json 2>/dev/null | jq -r '.params.max_accounts_per_did // "N/A"')
|
||||||
|
echo " Max accounts per DID: $MAX_ACCOUNTS"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "DEX module is not enabled"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 6: Check DID module
|
||||||
|
echo -e "${BLUE}Step 6: Checking DID module...${NC}"
|
||||||
|
|
||||||
|
if snrd query did params --node ${SONR_RPC} 2>/dev/null >/dev/null; then
|
||||||
|
print_status "DID module is available"
|
||||||
|
else
|
||||||
|
print_warning "DID module query failed (module may not be enabled)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 7: Create test configuration
|
||||||
|
echo -e "${BLUE}Step 7: Setting up test configuration...${NC}"
|
||||||
|
|
||||||
|
if [ ! -f "config.yaml" ]; then
|
||||||
|
if [ -f "config.example.yaml" ]; then
|
||||||
|
cp config.example.yaml config.yaml
|
||||||
|
|
||||||
|
# Update connection ID if different from default
|
||||||
|
if [ -n "$NOBLE_CONNECTION" ]; then
|
||||||
|
sed -i.bak "s/connection-0/$NOBLE_CONNECTION/g" config.yaml
|
||||||
|
rm -f config.yaml.bak
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Created config.yaml from example"
|
||||||
|
echo " Review and update config.yaml with your connection details"
|
||||||
|
else
|
||||||
|
print_warning "config.example.yaml not found"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "config.yaml already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 8: Install test dependencies
|
||||||
|
echo -e "${BLUE}Step 8: Installing test dependencies...${NC}"
|
||||||
|
|
||||||
|
if go mod download 2>/dev/null; then
|
||||||
|
print_status "Downloaded Go dependencies"
|
||||||
|
else
|
||||||
|
print_warning "Failed to download dependencies (may already be cached)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 9: Fund test accounts (optional)
|
||||||
|
echo -e "${BLUE}Step 9: Test account setup...${NC}"
|
||||||
|
|
||||||
|
read -p "Do you want to create and fund a test account? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
# Generate test account
|
||||||
|
TEST_ACCOUNT=$(snrd keys add test_e2e_user --keyring-backend test --output json 2>/dev/null | jq -r '.address' || echo "")
|
||||||
|
|
||||||
|
if [ -n "$TEST_ACCOUNT" ]; then
|
||||||
|
print_status "Created test account: $TEST_ACCOUNT"
|
||||||
|
|
||||||
|
# Try to fund via faucet
|
||||||
|
if curl -s "${FAUCET_URL}/health" >/dev/null 2>&1; then
|
||||||
|
FUND_RESPONSE=$(curl -s -X POST "${FAUCET_URL}/credit" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"address\":\"$TEST_ACCOUNT\",\"denom\":\"snr\"}" || echo "")
|
||||||
|
|
||||||
|
if echo "$FUND_RESPONSE" | grep -q "success"; then
|
||||||
|
print_status "Funded test account with SNR tokens"
|
||||||
|
else
|
||||||
|
print_warning "Failed to fund account via faucet"
|
||||||
|
echo " Fund manually with: snrd tx bank send validator $TEST_ACCOUNT 100000000usnr"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Failed to create test account"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 10: Run quick verification
|
||||||
|
echo -e "${BLUE}Step 10: Running quick verification...${NC}"
|
||||||
|
|
||||||
|
if make check-prereqs 2>/dev/null; then
|
||||||
|
print_status "All prerequisites verified"
|
||||||
|
else
|
||||||
|
print_warning "Some verification checks failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
echo -e "${BLUE}=== Setup Complete ===${NC}\n"
|
||||||
|
echo "Environment Summary:"
|
||||||
|
echo " Sonr RPC: $SONR_RPC"
|
||||||
|
echo " Sonr REST: $SONR_REST"
|
||||||
|
echo " Faucet: $FAUCET_URL"
|
||||||
|
echo " Chain ID: $CHAIN_ID"
|
||||||
|
echo " IBC Connections: $CONNECTIONS"
|
||||||
|
echo " IBC Channels: $CHANNELS"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Review and update config.yaml if needed"
|
||||||
|
echo " 2. Ensure Noble IBC connection is established"
|
||||||
|
echo " 3. Run tests with: make test"
|
||||||
|
echo ""
|
||||||
|
echo "Useful commands:"
|
||||||
|
echo " make test - Run all tests"
|
||||||
|
echo " make test-verbose - Run with detailed output"
|
||||||
|
echo " make check-ibc - Check IBC status"
|
||||||
|
echo " make help - Show all available commands"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if ready to run tests
|
||||||
|
if [ "$CONNECTIONS" -gt 0 ] && [ "$CHANNELS" -gt 0 ]; then
|
||||||
|
echo -e "${GREEN}✓ System appears ready for E2E tests!${NC}"
|
||||||
|
echo ""
|
||||||
|
read -p "Run tests now? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo ""
|
||||||
|
make test
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ IBC setup incomplete. Some tests may be skipped.${NC}"
|
||||||
|
echo " Set up IBC connections to Noble for full test coverage."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
package usdcswapdid
|
||||||
|
|
||||||
|
/*
|
||||||
|
USDC Swap E2E Tests - Implementation Notes
|
||||||
|
|
||||||
|
CURRENT STATUS:
|
||||||
|
These E2E tests are structured and ready but require manual setup steps before execution.
|
||||||
|
|
||||||
|
LIMITATION:
|
||||||
|
The test framework currently lacks full transaction signing/broadcasting capability.
|
||||||
|
The SignAndBroadcastTx method in StarshipClient is a placeholder that requires proper
|
||||||
|
keyring integration to function.
|
||||||
|
|
||||||
|
MANUAL SETUP REQUIRED:
|
||||||
|
Before running these tests, you must manually create the test resources using the CLI:
|
||||||
|
|
||||||
|
1. Create Test DID:
|
||||||
|
snrd tx did create-did did:snr:test_<timestamp> --from <account> --chain-id sonrtest_1-1 --yes
|
||||||
|
|
||||||
|
2. Register DEX Account:
|
||||||
|
snrd tx dex register-account did:snr:test_<timestamp> connection-noble \
|
||||||
|
--features swap,liquidity --from <account> --chain-id sonrtest_1-1 --yes
|
||||||
|
|
||||||
|
3. Wait for ICA channel handshake (1-2 minutes)
|
||||||
|
|
||||||
|
4. Execute Swaps:
|
||||||
|
snrd tx dex execute-swap did:snr:test_<timestamp> connection-noble \
|
||||||
|
usnr uusdc 1000000 950000 --from <account> --chain-id sonrtest_1-1 --yes
|
||||||
|
|
||||||
|
ALTERNATIVE APPROACH:
|
||||||
|
For automated E2E testing, consider:
|
||||||
|
- Using the setup.sh script to prepare the environment
|
||||||
|
- Implementing a test helper that shells out to the CLI
|
||||||
|
- Integrating with cosmos-sdk/testutil for full transaction support
|
||||||
|
- Using a dedicated test keyring with pre-configured keys
|
||||||
|
|
||||||
|
See README.md for detailed setup instructions and troubleshooting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cosmossdk.io/math"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/sonr-io/sonr/test/e2e/utils"
|
||||||
|
dextypes "github.com/sonr-io/sonr/x/dex/types"
|
||||||
|
didtypes "github.com/sonr-io/sonr/x/did/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Noble testnet configuration
|
||||||
|
NobleChainID = "noble-grand-1"
|
||||||
|
NobleConnectionID = "connection-noble" // Update with actual connection ID
|
||||||
|
NobleUSDCDenom = "uusdc"
|
||||||
|
|
||||||
|
// Test amounts (USDC has 6 decimals like native USDC)
|
||||||
|
USDCTestAmount = 1_000_000 // 1 USDC
|
||||||
|
USDCSwapAmount = 5_000_000 // 5 USDC
|
||||||
|
SNRTestAmount = 10_000_000 // 10 SNR
|
||||||
|
SNRSwapAmount = 1_000_000 // 1 SNR
|
||||||
|
|
||||||
|
// Slippage tolerance (5%)
|
||||||
|
SlippageTolerance = 0.05
|
||||||
|
)
|
||||||
|
|
||||||
|
// USDCSwapTestSuite is the main test suite for USDC swap functionality
|
||||||
|
type USDCSwapTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
cfg *utils.TestConfig
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
testDID string
|
||||||
|
testAccount string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite runs once before all tests
|
||||||
|
func (s *USDCSwapTestSuite) SetupSuite() {
|
||||||
|
s.cfg = utils.NewTestConfig()
|
||||||
|
s.ctx, s.cancel = context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
|
||||||
|
// Setup test account
|
||||||
|
users := utils.SetupTestUsers(s.T(), s.cfg, math.NewInt(SNRTestAmount))
|
||||||
|
require.Len(s.T(), users, 1, "should create at least one test user")
|
||||||
|
s.testAccount = users[0].Address
|
||||||
|
|
||||||
|
s.T().Logf("Test suite initialized with account: %s", s.testAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite runs once after all tests
|
||||||
|
func (s *USDCSwapTestSuite) TearDownSuite() {
|
||||||
|
if s.cancel != nil {
|
||||||
|
s.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTest runs before each test
|
||||||
|
func (s *USDCSwapTestSuite) SetupTest() {
|
||||||
|
// Create unique DID for each test
|
||||||
|
s.testDID = fmt.Sprintf("did:snr:test_%d", time.Now().UnixNano())
|
||||||
|
s.T().Logf("Created test DID: %s", s.testDID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUSDCSwapE2E is the main end-to-end test function
|
||||||
|
func TestUSDCSwapE2E(t *testing.T) {
|
||||||
|
suite.Run(t, new(USDCSwapTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test01_VerifyNobleConfiguration verifies Noble chain configuration
|
||||||
|
func (s *USDCSwapTestSuite) Test01_VerifyNobleConfiguration() {
|
||||||
|
s.T().Log("=== Test 01: Verify Noble Configuration ===")
|
||||||
|
|
||||||
|
// Verify Noble configuration constants
|
||||||
|
require.Equal(s.T(), NobleChainID, dextypes.NobleTestnetChainID,
|
||||||
|
"Noble chain ID should match")
|
||||||
|
require.Equal(s.T(), NobleUSDCDenom, dextypes.NobleUSDCDenom,
|
||||||
|
"USDC denom should match")
|
||||||
|
require.Equal(s.T(), uint8(6), dextypes.NobleUSDCDecimals,
|
||||||
|
"USDC decimals should be 6")
|
||||||
|
|
||||||
|
// Verify Noble is recognized as a Noble chain
|
||||||
|
require.True(s.T(), dextypes.IsNobleChain(NobleChainID),
|
||||||
|
"Noble testnet chain ID should be recognized")
|
||||||
|
|
||||||
|
s.T().Log("✓ Noble configuration verified successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test02_CreateDIDDocument creates a DID document for testing
|
||||||
|
func (s *USDCSwapTestSuite) Test02_CreateDIDDocument() {
|
||||||
|
s.T().Log("=== Test 02: Create DID Document ===")
|
||||||
|
|
||||||
|
// Create DID document
|
||||||
|
did, err := CreateTestDID(s.ctx, s.cfg, s.testAccount, s.testDID)
|
||||||
|
require.NoError(s.T(), err, "failed to create DID document")
|
||||||
|
require.NotNil(s.T(), did, "DID document should not be nil")
|
||||||
|
require.Equal(s.T(), s.testDID, did.Id, "DID ID should match")
|
||||||
|
|
||||||
|
s.T().Logf("✓ DID document created: %s", did.Id)
|
||||||
|
|
||||||
|
// Verify DID can be queried
|
||||||
|
queriedDID, err := QueryDID(s.ctx, s.cfg, s.testDID)
|
||||||
|
require.NoError(s.T(), err, "failed to query DID")
|
||||||
|
require.NotNil(s.T(), queriedDID, "queried DID should not be nil")
|
||||||
|
require.Equal(s.T(), s.testDID, queriedDID.Id, "queried DID should match")
|
||||||
|
|
||||||
|
s.T().Log("✓ DID query verification successful")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test03_RegisterDEXAccount registers a DEX account on Noble connection
|
||||||
|
func (s *USDCSwapTestSuite) Test03_RegisterDEXAccount() {
|
||||||
|
s.T().Log("=== Test 03: Register DEX Account ===")
|
||||||
|
|
||||||
|
// First create DID
|
||||||
|
_, err := CreateTestDID(s.ctx, s.cfg, s.testAccount, s.testDID)
|
||||||
|
require.NoError(s.T(), err, "failed to create DID")
|
||||||
|
|
||||||
|
// Register DEX account
|
||||||
|
msg := &dextypes.MsgRegisterDEXAccount{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
Features: []string{"swap", "liquidity"},
|
||||||
|
Metadata: "Noble USDC swap test account",
|
||||||
|
}
|
||||||
|
|
||||||
|
txResp, err := s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, msg)
|
||||||
|
require.NoError(s.T(), err, "failed to register DEX account")
|
||||||
|
require.Equal(s.T(), uint32(0), txResp.Code, "transaction should succeed")
|
||||||
|
|
||||||
|
s.T().Logf("✓ DEX account registered, tx hash: %s", txResp.TxHash)
|
||||||
|
|
||||||
|
// Wait for account activation
|
||||||
|
time.Sleep(2 * s.cfg.BlockTime)
|
||||||
|
|
||||||
|
// Query the created account
|
||||||
|
account, err := QueryDEXAccount(s.ctx, s.cfg, s.testDID, NobleConnectionID)
|
||||||
|
require.NoError(s.T(), err, "failed to query DEX account")
|
||||||
|
require.NotNil(s.T(), account, "DEX account should exist")
|
||||||
|
require.Equal(s.T(), s.testDID, account.Did, "DID should match")
|
||||||
|
require.Equal(s.T(), NobleConnectionID, account.ConnectionId, "connection should match")
|
||||||
|
|
||||||
|
// Account may be pending if ICA channel is not yet established
|
||||||
|
s.T().Logf("✓ DEX account status: %s", account.Status.String())
|
||||||
|
s.T().Logf("✓ ICA port ID: %s", account.PortId)
|
||||||
|
|
||||||
|
if account.AccountAddress != "" {
|
||||||
|
s.T().Logf("✓ ICA address: %s", account.AccountAddress)
|
||||||
|
} else {
|
||||||
|
s.T().Log("⚠ ICA address pending (channel handshake may be in progress)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test04_ExecuteUSDCSwap_SNRToUSDC tests swapping SNR to USDC
|
||||||
|
func (s *USDCSwapTestSuite) Test04_ExecuteUSDCSwap_SNRToUSDC() {
|
||||||
|
s.T().Log("=== Test 04: Execute USDC Swap (SNR → USDC) ===")
|
||||||
|
|
||||||
|
// Setup: Create DID and register DEX account
|
||||||
|
s.setupDEXAccount()
|
||||||
|
|
||||||
|
// Get initial balances
|
||||||
|
initialSNR := s.getBalance(s.testAccount, s.cfg.StakingDenom)
|
||||||
|
s.T().Logf("Initial SNR balance: %s", initialSNR.String())
|
||||||
|
|
||||||
|
// Calculate expected output (95% of input for 5% slippage)
|
||||||
|
swapAmount := math.NewInt(SNRSwapAmount)
|
||||||
|
minUSDCOut := math.NewInt(SNRSwapAmount).
|
||||||
|
MulRaw(95).QuoRaw(100) // 5% slippage tolerance
|
||||||
|
|
||||||
|
// Execute swap
|
||||||
|
swapMsg := &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: NobleUSDCDenom,
|
||||||
|
Amount: swapAmount,
|
||||||
|
MinAmountOut: minUSDCOut,
|
||||||
|
Route: fmt.Sprintf("noble:%s", NobleConnectionID),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
txResp, err := s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, swapMsg)
|
||||||
|
require.NoError(s.T(), err, "failed to execute swap")
|
||||||
|
require.Equal(s.T(), uint32(0), txResp.Code, "swap transaction should succeed")
|
||||||
|
|
||||||
|
s.T().Logf("✓ Swap transaction broadcasted, tx hash: %s", txResp.TxHash)
|
||||||
|
|
||||||
|
// Verify swap event was emitted
|
||||||
|
s.verifySwapEvent(txResp.TxHash, s.testDID, NobleConnectionID)
|
||||||
|
|
||||||
|
// Note: In a real IBC swap, we would need to wait for ICA callback
|
||||||
|
// and verify balances on both chains. For this test, we verify the
|
||||||
|
// transaction was accepted and properly formatted.
|
||||||
|
s.T().Log("✓ Swap execution test completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test05_ExecuteUSDCSwap_USDCToSNR tests swapping USDC to SNR
|
||||||
|
func (s *USDCSwapTestSuite) Test05_ExecuteUSDCSwap_USDCToSNR() {
|
||||||
|
s.T().Log("=== Test 05: Execute USDC Swap (USDC → SNR) ===")
|
||||||
|
|
||||||
|
// Setup: Create DID and register DEX account
|
||||||
|
s.setupDEXAccount()
|
||||||
|
|
||||||
|
// Calculate expected output
|
||||||
|
swapAmount := math.NewInt(USDCSwapAmount)
|
||||||
|
minSNROut := math.NewInt(USDCSwapAmount).
|
||||||
|
MulRaw(95).QuoRaw(100) // 5% slippage tolerance
|
||||||
|
|
||||||
|
// Execute swap
|
||||||
|
swapMsg := &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: NobleUSDCDenom,
|
||||||
|
TargetDenom: s.cfg.StakingDenom,
|
||||||
|
Amount: swapAmount,
|
||||||
|
MinAmountOut: minSNROut,
|
||||||
|
Route: fmt.Sprintf("noble:%s", NobleConnectionID),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
txResp, err := s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, swapMsg)
|
||||||
|
require.NoError(s.T(), err, "failed to execute swap")
|
||||||
|
require.Equal(s.T(), uint32(0), txResp.Code, "swap transaction should succeed")
|
||||||
|
|
||||||
|
s.T().Logf("✓ Swap transaction broadcasted, tx hash: %s", txResp.TxHash)
|
||||||
|
|
||||||
|
// Verify swap event
|
||||||
|
s.verifySwapEvent(txResp.TxHash, s.testDID, NobleConnectionID)
|
||||||
|
|
||||||
|
s.T().Log("✓ Reverse swap execution test completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test06_SwapWithInvalidParameters tests swap with invalid parameters
|
||||||
|
func (s *USDCSwapTestSuite) Test06_SwapWithInvalidParameters() {
|
||||||
|
s.T().Log("=== Test 06: Swap With Invalid Parameters ===")
|
||||||
|
|
||||||
|
// Setup: Create DID and register DEX account
|
||||||
|
s.setupDEXAccount()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
msg *dextypes.MsgExecuteSwap
|
||||||
|
expectedErr bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero_amount",
|
||||||
|
msg: &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: NobleUSDCDenom,
|
||||||
|
Amount: math.ZeroInt(),
|
||||||
|
MinAmountOut: math.NewInt(1000),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
},
|
||||||
|
expectedErr: true,
|
||||||
|
description: "swap with zero amount should fail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same_denom",
|
||||||
|
msg: &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: s.cfg.StakingDenom,
|
||||||
|
Amount: math.NewInt(1000),
|
||||||
|
MinAmountOut: math.NewInt(900),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
},
|
||||||
|
expectedErr: true,
|
||||||
|
description: "swapping same denom should fail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative_min_out",
|
||||||
|
msg: &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: NobleUSDCDenom,
|
||||||
|
Amount: math.NewInt(1000),
|
||||||
|
MinAmountOut: math.NewInt(-100),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
},
|
||||||
|
expectedErr: true,
|
||||||
|
description: "negative min amount out should fail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_connection",
|
||||||
|
msg: &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: "connection-invalid-999",
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: NobleUSDCDenom,
|
||||||
|
Amount: math.NewInt(1000),
|
||||||
|
MinAmountOut: math.NewInt(900),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
},
|
||||||
|
expectedErr: true,
|
||||||
|
description: "invalid connection ID should fail",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Logf("Testing: %s", tc.description)
|
||||||
|
|
||||||
|
txResp, err := s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, tc.msg)
|
||||||
|
|
||||||
|
if tc.expectedErr {
|
||||||
|
// Transaction should either fail to broadcast or have non-zero code
|
||||||
|
if err == nil {
|
||||||
|
require.NotEqual(t, uint32(0), txResp.Code,
|
||||||
|
"transaction should fail with non-zero code")
|
||||||
|
t.Logf("✓ Transaction failed as expected with code: %d, log: %s",
|
||||||
|
txResp.Code, txResp.RawLog)
|
||||||
|
} else {
|
||||||
|
t.Logf("✓ Transaction failed as expected with error: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err, "transaction should succeed")
|
||||||
|
require.Equal(t, uint32(0), txResp.Code, "transaction should have code 0")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
s.T().Log("✓ Invalid parameter tests completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test07_SwapWithUCANPermission tests swap with UCAN authorization
|
||||||
|
func (s *USDCSwapTestSuite) Test07_SwapWithUCANPermission() {
|
||||||
|
s.T().Skip("UCAN integration requires full UCAN module setup")
|
||||||
|
s.T().Log("=== Test 07: Swap With UCAN Permission ===")
|
||||||
|
|
||||||
|
// Setup: Create DID and register DEX account
|
||||||
|
s.setupDEXAccount()
|
||||||
|
|
||||||
|
// TODO: Generate UCAN token with swap capabilities
|
||||||
|
// ucanToken := generateUCANToken(s.testDID, "swap", NobleConnectionID)
|
||||||
|
|
||||||
|
swapMsg := &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: NobleUSDCDenom,
|
||||||
|
Amount: math.NewInt(SNRSwapAmount),
|
||||||
|
MinAmountOut: math.NewInt(SNRSwapAmount * 95 / 100),
|
||||||
|
UcanToken: "", // Would be populated with actual token
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
txResp, err := s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, swapMsg)
|
||||||
|
require.NoError(s.T(), err, "failed to execute swap with UCAN")
|
||||||
|
require.Equal(s.T(), uint32(0), txResp.Code, "swap with UCAN should succeed")
|
||||||
|
|
||||||
|
s.T().Log("✓ UCAN-authorized swap test completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test08_QueryDEXHistory queries transaction history
|
||||||
|
func (s *USDCSwapTestSuite) Test08_QueryDEXHistory() {
|
||||||
|
s.T().Log("=== Test 08: Query DEX History ===")
|
||||||
|
|
||||||
|
// Setup and execute a swap
|
||||||
|
s.setupDEXAccount()
|
||||||
|
s.executeTestSwap()
|
||||||
|
|
||||||
|
// Query DEX history
|
||||||
|
history, err := QueryDEXHistory(s.ctx, s.cfg, s.testDID)
|
||||||
|
require.NoError(s.T(), err, "failed to query DEX history")
|
||||||
|
|
||||||
|
if len(history) > 0 {
|
||||||
|
s.T().Logf("✓ Found %d transactions in history", len(history))
|
||||||
|
for i, activity := range history {
|
||||||
|
s.T().Logf(" [%d] Type: %s, Status: %s, Height: %d",
|
||||||
|
i+1, activity.Type, activity.Status, activity.BlockHeight)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.T().Log("⚠ No history found (ICA may not be fully configured)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test09_MultipleSwaps tests executing multiple swaps in sequence
|
||||||
|
func (s *USDCSwapTestSuite) Test09_MultipleSwaps() {
|
||||||
|
s.T().Log("=== Test 09: Multiple Sequential Swaps ===")
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
s.setupDEXAccount()
|
||||||
|
|
||||||
|
numSwaps := 3
|
||||||
|
for i := 0; i < numSwaps; i++ {
|
||||||
|
s.T().Logf("Executing swap %d/%d", i+1, numSwaps)
|
||||||
|
|
||||||
|
swapMsg := &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: NobleUSDCDenom,
|
||||||
|
Amount: math.NewInt(100_000), // Small amounts for multiple swaps
|
||||||
|
MinAmountOut: math.NewInt(95_000),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
txResp, err := s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, swapMsg)
|
||||||
|
require.NoError(s.T(), err, "swap %d should succeed", i+1)
|
||||||
|
require.Equal(s.T(), uint32(0), txResp.Code, "swap %d should have code 0", i+1)
|
||||||
|
|
||||||
|
s.T().Logf("✓ Swap %d completed, tx: %s", i+1, txResp.TxHash)
|
||||||
|
|
||||||
|
// Small delay between swaps
|
||||||
|
time.Sleep(s.cfg.BlockTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.T().Logf("✓ Successfully executed %d swaps", numSwaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Functions
|
||||||
|
|
||||||
|
func (s *USDCSwapTestSuite) setupDEXAccount() {
|
||||||
|
// Create DID if not exists
|
||||||
|
_, err := CreateTestDID(s.ctx, s.cfg, s.testAccount, s.testDID)
|
||||||
|
if err != nil {
|
||||||
|
// DID might already exist, check query
|
||||||
|
_, queryErr := QueryDID(s.ctx, s.cfg, s.testDID)
|
||||||
|
require.NoError(s.T(), queryErr, "DID should exist or be creatable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register DEX account
|
||||||
|
msg := &dextypes.MsgRegisterDEXAccount{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
Features: []string{"swap"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, msg)
|
||||||
|
// Ignore error if account already exists
|
||||||
|
|
||||||
|
// Wait for registration
|
||||||
|
time.Sleep(2 * s.cfg.BlockTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *USDCSwapTestSuite) executeTestSwap() {
|
||||||
|
swapMsg := &dextypes.MsgExecuteSwap{
|
||||||
|
Did: s.testDID,
|
||||||
|
ConnectionId: NobleConnectionID,
|
||||||
|
SourceDenom: s.cfg.StakingDenom,
|
||||||
|
TargetDenom: NobleUSDCDenom,
|
||||||
|
Amount: math.NewInt(SNRSwapAmount),
|
||||||
|
MinAmountOut: math.NewInt(SNRSwapAmount * 95 / 100),
|
||||||
|
Timeout: time.Now().Add(60 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
txResp, err := s.cfg.Client.SignAndBroadcastTx(s.ctx, s.testAccount, swapMsg)
|
||||||
|
require.NoError(s.T(), err, "test swap should succeed")
|
||||||
|
require.Equal(s.T(), uint32(0), txResp.Code, "test swap should have code 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *USDCSwapTestSuite) getBalance(address, denom string) math.Int {
|
||||||
|
balance, err := s.cfg.Client.GetBalance(s.ctx, address, denom)
|
||||||
|
require.NoError(s.T(), err, "should query balance")
|
||||||
|
return balance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *USDCSwapTestSuite) verifySwapEvent(txHash, expectedDID, expectedConnection string) {
|
||||||
|
// Query events for the transaction
|
||||||
|
height, err := s.cfg.Client.GetLatestBlockHeight(s.ctx)
|
||||||
|
require.NoError(s.T(), err, "should get block height")
|
||||||
|
|
||||||
|
// Search for swap events
|
||||||
|
events, err := s.cfg.Client.QueryEventsByType(s.ctx,
|
||||||
|
dextypes.EventTypeSwapExecuted, height-10, height)
|
||||||
|
|
||||||
|
if err != nil || len(events.Events) == 0 {
|
||||||
|
s.T().Log("⚠ Swap event not found (may be indexed later)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify event contains expected attributes
|
||||||
|
for _, event := range events.Events {
|
||||||
|
for _, attr := range event.Attributes {
|
||||||
|
if attr.Key == "did" && attr.Value == expectedDID {
|
||||||
|
s.T().Logf("✓ Found swap event for DID: %s", expectedDID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.T().Log("⚠ Swap event attributes not yet indexed")
|
||||||
|
}
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
# Noble Testnet Integration for Sonr DEX Module
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the integration of the Noble testnet with Sonr's x/dex module. Noble is a Cosmos appchain purpose-built for native asset issuance, particularly USDC, in the IBC ecosystem.
|
||||||
|
|
||||||
|
## What is Noble?
|
||||||
|
|
||||||
|
Noble is an application-specific blockchain built on the Cosmos SDK that serves as the canonical issuance hub for USDC and other real-world assets (RWAs) in the Cosmos ecosystem. Key features:
|
||||||
|
|
||||||
|
- **Native USDC Issuance**: Circle's official USDC is natively issued on Noble
|
||||||
|
- **IBC Connectivity**: Seamlessly transfers assets across 50+ IBC-enabled chains
|
||||||
|
- **Packet Forwarding**: Enables "1-click" asset transfers between chains
|
||||||
|
- **CCTP Integration**: Circle's Cross-Chain Transfer Protocol for bridging from EVM chains
|
||||||
|
|
||||||
|
## Noble Testnet Configuration
|
||||||
|
|
||||||
|
### Chain Details
|
||||||
|
|
||||||
|
- **Chain ID**: `noble-grand-1`
|
||||||
|
- **RPC Endpoint**: `https://noble-testnet-rpc.polkachu.com:443`
|
||||||
|
- **gRPC Endpoint**: `noble-testnet-grpc.polkachu.com:21590`
|
||||||
|
- **USDC Denomination**: `uusdc` (micro-USDC, 6 decimals)
|
||||||
|
|
||||||
|
### Default Configuration
|
||||||
|
|
||||||
|
The Noble testnet is included in the default DEX module parameters:
|
||||||
|
|
||||||
|
```go
|
||||||
|
AllowedConnections: []string{
|
||||||
|
"noble-grand-1", // Noble testnet - USDC hub
|
||||||
|
"osmo-test-5", // Osmosis testnet
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Features
|
||||||
|
|
||||||
|
### 1. USDC Helper Functions
|
||||||
|
|
||||||
|
The integration includes helper functions for working with USDC:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Convert base units to USDC
|
||||||
|
usdcAmount := ConvertToUSDC(sdk.NewInt(1500000)) // Returns 1.5 USDC
|
||||||
|
|
||||||
|
// Convert USDC to base units
|
||||||
|
baseUnits := ConvertFromUSDC(sdk.MustNewDecFromStr("1.5")) // Returns 1500000
|
||||||
|
|
||||||
|
// Format for display
|
||||||
|
formatted := FormatUSDCAmount(sdk.NewInt(1500000)) // Returns "1.500000 USDC"
|
||||||
|
|
||||||
|
// Parse from string
|
||||||
|
amount, err := ParseUSDCAmount("1.5") // Returns 1500000 base units
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Chain Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get Noble testnet configuration
|
||||||
|
config := GetNobleTestnetConfig()
|
||||||
|
// Returns: NobleChainConfig{
|
||||||
|
// ChainID: "noble-grand-1",
|
||||||
|
// RPCEndpoint: "https://noble-testnet-rpc.polkachu.com:443",
|
||||||
|
// GRPCEndpoint: "noble-testnet-grpc.polkachu.com:21590",
|
||||||
|
// USDCDenom: "uusdc",
|
||||||
|
// Decimals: 6,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Check if a chain is Noble
|
||||||
|
isNoble := IsNobleChain("noble-grand-1") // Returns true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Trading Pairs
|
||||||
|
|
||||||
|
Common USDC trading pairs are predefined:
|
||||||
|
|
||||||
|
```go
|
||||||
|
pairs := GetNobleUSDCPairs()
|
||||||
|
// Returns pairs like:
|
||||||
|
// - ATOM/USDC
|
||||||
|
// - OSMO/USDC
|
||||||
|
// - AKT/USDC
|
||||||
|
// - JUNO/USDC
|
||||||
|
// - STARS/USDC
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Swap Parameters
|
||||||
|
|
||||||
|
Structured parameters for Noble swaps:
|
||||||
|
|
||||||
|
```go
|
||||||
|
swapParams := NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000), // 1 ATOM
|
||||||
|
MinOutput: sdk.NewInt(5000000), // Min 5 USDC
|
||||||
|
Receiver: "sonr1...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := swapParams.Validate(); err != nil {
|
||||||
|
// Handle validation error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Liquidity Parameters
|
||||||
|
|
||||||
|
Parameters for providing liquidity:
|
||||||
|
|
||||||
|
```go
|
||||||
|
liquidityParams := NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000), // 1 USDC
|
||||||
|
Amount1: sdk.NewInt(100000), // 0.1 ATOM
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := liquidityParams.Validate(); err != nil {
|
||||||
|
// Handle validation error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Registering a DEX Account for Noble
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Register an Interchain Account on Noble testnet
|
||||||
|
sonrd tx dex register-dex-account \
|
||||||
|
did:sonr:user:abc123 \
|
||||||
|
connection-0 \
|
||||||
|
noble-grand-1 \
|
||||||
|
--from mykey \
|
||||||
|
--chain-id sonr_1-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Executing a Swap via Noble
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Swap ATOM for USDC on Noble
|
||||||
|
sonrd tx dex execute-swap \
|
||||||
|
did:sonr:user:abc123 \
|
||||||
|
connection-0 \
|
||||||
|
uatom \
|
||||||
|
uusdc \
|
||||||
|
1000000 \
|
||||||
|
5000000 \
|
||||||
|
--from mykey \
|
||||||
|
--chain-id sonr_1-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying USDC Balance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query USDC balance on Noble for a DEX account
|
||||||
|
sonrd query dex balance \
|
||||||
|
did:sonr:user:abc123 \
|
||||||
|
connection-0 \
|
||||||
|
uusdc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Parameters
|
||||||
|
|
||||||
|
The DEX module includes the following default parameters for Noble integration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
params:
|
||||||
|
enabled: true
|
||||||
|
max_accounts_per_did: 5
|
||||||
|
default_timeout_seconds: 600
|
||||||
|
allowed_connections:
|
||||||
|
- noble-grand-1 # Noble testnet
|
||||||
|
- osmo-test-5 # Osmosis testnet
|
||||||
|
min_swap_amount: "1000"
|
||||||
|
max_daily_volume: "1000000000000"
|
||||||
|
rate_limits:
|
||||||
|
max_ops_per_block: 10
|
||||||
|
max_ops_per_did_per_day: 100
|
||||||
|
cooldown_blocks: 5
|
||||||
|
fees:
|
||||||
|
swap_fee_bps: 30 # 0.3%
|
||||||
|
liquidity_fee_bps: 20 # 0.2%
|
||||||
|
order_fee_bps: 10 # 0.1%
|
||||||
|
fee_collector: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## IBC Connection Setup
|
||||||
|
|
||||||
|
To establish an IBC connection with Noble testnet:
|
||||||
|
|
||||||
|
### 1. Create IBC Client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create IBC client for Noble on Sonr chain
|
||||||
|
hermes create client \
|
||||||
|
--host-chain sonr_1-1 \
|
||||||
|
--reference-chain noble-grand-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create IBC Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create IBC connection
|
||||||
|
hermes create connection \
|
||||||
|
--a-chain sonr_1-1 \
|
||||||
|
--b-chain noble-grand-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Transfer Channel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create transfer channel
|
||||||
|
hermes create channel \
|
||||||
|
--a-chain sonr_1-1 \
|
||||||
|
--a-connection connection-0 \
|
||||||
|
--a-port transfer \
|
||||||
|
--b-port transfer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query IBC connections
|
||||||
|
sonrd query ibc connection connections
|
||||||
|
|
||||||
|
# Query IBC channels
|
||||||
|
sonrd query ibc channel channels
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### 1. Connection Whitelisting
|
||||||
|
|
||||||
|
Only connections listed in `allowed_connections` parameter can be used:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ValidateNobleConnection(connectionID string, allowedConnections []string) error {
|
||||||
|
for _, allowed := range allowedConnections {
|
||||||
|
if connectionID == allowed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("connection %s not in allowed connections list", connectionID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Rate Limiting
|
||||||
|
|
||||||
|
The module implements rate limiting to prevent abuse:
|
||||||
|
|
||||||
|
- **Per Block**: Maximum 10 operations per block
|
||||||
|
- **Per DID**: Maximum 100 operations per day
|
||||||
|
- **Cooldown**: 5 block cooldown between operations
|
||||||
|
|
||||||
|
### 3. Amount Validation
|
||||||
|
|
||||||
|
All amounts are validated before execution:
|
||||||
|
|
||||||
|
- Minimum swap amount: 1000 base units
|
||||||
|
- Maximum daily volume per DID: 1,000,000,000,000 base units
|
||||||
|
- Positive amount checks on all operations
|
||||||
|
|
||||||
|
### 4. UCAN Authorization
|
||||||
|
|
||||||
|
All DEX operations require valid UCAN tokens:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// UCAN capabilities for DEX operations
|
||||||
|
capabilities := []string{
|
||||||
|
"dex/swap",
|
||||||
|
"dex/liquidity/provide",
|
||||||
|
"dex/liquidity/remove",
|
||||||
|
"dex/order/create",
|
||||||
|
"dex/order/cancel",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run DEX module tests
|
||||||
|
cd x/dex
|
||||||
|
go test -v ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run end-to-end tests
|
||||||
|
cd test/e2e
|
||||||
|
go test -v -run TestDEXModuleOperations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
1. **Start local testnet**:
|
||||||
|
```bash
|
||||||
|
cd networks/testnet
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Register account**:
|
||||||
|
```bash
|
||||||
|
sonrd tx dex register-dex-account did:sonr:test connection-0 noble-grand-1 --from test
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Execute swap**:
|
||||||
|
```bash
|
||||||
|
sonrd tx dex execute-swap did:sonr:test connection-0 uatom uusdc 1000000 500000 --from test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Testnet Integration ✅
|
||||||
|
- [x] Add Noble testnet to allowed connections
|
||||||
|
- [x] Create USDC helper functions
|
||||||
|
- [x] Implement chain configuration
|
||||||
|
- [x] Add documentation
|
||||||
|
|
||||||
|
### Phase 2: Enhanced Features 🚧
|
||||||
|
- [ ] Implement actual ICA swap execution
|
||||||
|
- [ ] Add multi-hop routing via USDC
|
||||||
|
- [ ] Integrate with Osmosis pools
|
||||||
|
- [ ] Add limit order support
|
||||||
|
|
||||||
|
### Phase 3: Mainnet Deployment 📋
|
||||||
|
- [ ] Security audit
|
||||||
|
- [ ] Add Noble mainnet configuration
|
||||||
|
- [ ] Implement advanced slippage protection
|
||||||
|
- [ ] Add monitoring and alerting
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Noble Documentation
|
||||||
|
- **Developer Hub**: https://www.noble.xyz/dev-hub
|
||||||
|
- **Docs**: https://docs.noble.xyz
|
||||||
|
- **GitHub**: https://github.com/noble-assets/noble
|
||||||
|
|
||||||
|
### Cosmos IBC
|
||||||
|
- **IBC Protocol**: https://ibc.cosmos.network
|
||||||
|
- **ICA Controller**: https://github.com/cosmos/ibc-go/tree/main/modules/apps/27-interchain-accounts
|
||||||
|
|
||||||
|
### Circle USDC
|
||||||
|
- **USDC on Noble**: https://www.circle.com/multi-chain-usdc/noble
|
||||||
|
- **CCTP**: https://www.circle.com/en/cross-chain-transfer-protocol
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For questions or issues with the Noble integration:
|
||||||
|
|
||||||
|
1. Check the [DEX Module README](README.md)
|
||||||
|
2. Review the [Noble documentation](https://docs.noble.xyz)
|
||||||
|
3. Open an issue on GitHub
|
||||||
|
4. Join the Sonr Discord community
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This integration is part of the Sonr blockchain and follows the same license terms.
|
||||||
+30
-6
@@ -75,9 +75,18 @@ func CmdRegisterDEXAccount() *cobra.Command {
|
|||||||
// CmdExecuteSwap returns a command to execute a swap
|
// CmdExecuteSwap returns a command to execute a swap
|
||||||
func CmdExecuteSwap() *cobra.Command {
|
func CmdExecuteSwap() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "swap [did] [connection-id] [token-in] [token-out-denom] [min-amount-out] [pool-id]",
|
Use: "swap [did] [connection-id] [token-in] [token-out-denom] [min-amount-out]",
|
||||||
Short: "Execute a token swap through ICA",
|
Short: "Execute a token swap through ICA (supports Noble USDC)",
|
||||||
Args: cobra.ExactArgs(6),
|
Long: `Execute a token swap on a remote chain via Interchain Accounts.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Swap 1000000 uatom for USDC on Noble testnet
|
||||||
|
snrd tx dex swap did:snr:user1 connection-0 1000000uatom uusdc 950000 --ucan-token="..." --timeout=60s
|
||||||
|
|
||||||
|
# Swap USDC for ATOM on Osmosis
|
||||||
|
snrd tx dex swap did:snr:user1 connection-1 1000000uusdc uatom 950000 --route="pool:1"
|
||||||
|
`,
|
||||||
|
Args: cobra.ExactArgs(5),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
clientCtx, err := client.GetClientTxContext(cmd)
|
clientCtx, err := client.GetClientTxContext(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -99,9 +108,18 @@ func CmdExecuteSwap() *cobra.Command {
|
|||||||
return fmt.Errorf("invalid min-amount-out: %s", args[4])
|
return fmt.Errorf("invalid min-amount-out: %s", args[4])
|
||||||
}
|
}
|
||||||
|
|
||||||
poolID, err := strconv.ParseUint(args[5], 10, 64)
|
// Parse optional flags
|
||||||
|
ucanToken, _ := cmd.Flags().GetString("ucan-token")
|
||||||
|
route, _ := cmd.Flags().GetString("route")
|
||||||
|
timeoutStr, _ := cmd.Flags().GetString("timeout")
|
||||||
|
|
||||||
|
// Parse timeout duration
|
||||||
|
timeoutDuration := 30 * time.Second // default
|
||||||
|
if timeoutStr != "" {
|
||||||
|
timeoutDuration, err = time.ParseDuration(timeoutStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid pool-id: %w", err)
|
return fmt.Errorf("invalid timeout: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := &types.MsgExecuteSwap{
|
msg := &types.MsgExecuteSwap{
|
||||||
@@ -111,7 +129,9 @@ func CmdExecuteSwap() *cobra.Command {
|
|||||||
TargetDenom: tokenOutDenom,
|
TargetDenom: tokenOutDenom,
|
||||||
Amount: tokenIn.Amount,
|
Amount: tokenIn.Amount,
|
||||||
MinAmountOut: minAmountOut,
|
MinAmountOut: minAmountOut,
|
||||||
Route: fmt.Sprintf("pool:%d", poolID),
|
Route: route,
|
||||||
|
UcanToken: ucanToken,
|
||||||
|
Timeout: time.Now().Add(timeoutDuration),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := msg.ValidateBasic(); err != nil {
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
@@ -122,6 +142,10 @@ func CmdExecuteSwap() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().String("ucan-token", "", "UCAN authorization token for permission delegation")
|
||||||
|
cmd.Flags().String("route", "", "Optional specific swap route (e.g., 'pool:1' or 'noble:channel-0')")
|
||||||
|
cmd.Flags().String("timeout", "30s", "Timeout duration for the swap (e.g., '30s', '1m')")
|
||||||
|
|
||||||
flags.AddTxFlagsToCmd(cmd)
|
flags.AddTxFlagsToCmd(cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
+145
-21
@@ -2,7 +2,10 @@ package keeper
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sdkerrors "cosmossdk.io/errors"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
"github.com/sonr-io/sonr/x/dex/types"
|
"github.com/sonr-io/sonr/x/dex/types"
|
||||||
)
|
)
|
||||||
@@ -52,23 +55,15 @@ func (ms msgServer) RegisterDEXAccount(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ExecuteSwap - Implement cross-chain swap execution via ICA
|
// ExecuteSwap implements cross-chain swap execution via ICA
|
||||||
// This method should handle token swaps on remote chains through Interchain Accounts
|
// This method handles token swaps on remote chains through Interchain Accounts
|
||||||
// Required implementation steps:
|
// with special support for Noble USDC integration
|
||||||
// 1. Validate the sender's DID exists and is active using did keeper
|
|
||||||
// 2. Verify UCAN token has proper swap capabilities (resource: swap, action: execute)
|
|
||||||
// 3. Retrieve the ICA account for this DID and connection from state
|
|
||||||
// 4. Build the appropriate swap message for the target chain's DEX protocol
|
|
||||||
// 5. Create ICA packet data with the swap transaction
|
|
||||||
// 6. Send ICA packet through IBC channel and await acknowledgment
|
|
||||||
// 7. Store transaction details in DWN for user history tracking
|
|
||||||
// 8. Emit events for indexing and monitoring
|
|
||||||
// Returns: Sequence number and transaction ID on success
|
|
||||||
// ExecuteSwap implements types.MsgServer.
|
|
||||||
func (ms msgServer) ExecuteSwap(
|
func (ms msgServer) ExecuteSwap(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
msg *types.MsgExecuteSwap,
|
msg *types.MsgExecuteSwap,
|
||||||
) (*types.MsgExecuteSwapResponse, error) {
|
) (*types.MsgExecuteSwapResponse, error) {
|
||||||
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
||||||
|
|
||||||
// Validate UCAN permission if token provided
|
// Validate UCAN permission if token provided
|
||||||
if msg.UcanToken != "" {
|
if msg.UcanToken != "" {
|
||||||
// Use connection ID as resource ID for swap operations
|
// Use connection ID as resource ID for swap operations
|
||||||
@@ -77,13 +72,119 @@ func (ms msgServer) ExecuteSwap(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement swap execution via ICA
|
// 1. Validate DID exists and is active
|
||||||
// 1. Validate DID
|
_, err := ms.didKeeper.GetDIDDocument(sdkCtx, msg.Did)
|
||||||
// 2. Get ICA account for this DID and connection
|
if err != nil {
|
||||||
// 3. Construct swap message for remote chain
|
return nil, sdkerrors.Wrapf(err, "DID %s not found", msg.Did)
|
||||||
// 4. Send ICA packet with swap instruction
|
}
|
||||||
// 5. Track transaction in DWN
|
// Note: Active status check removed as DIDDocument may not have Active field
|
||||||
return &types.MsgExecuteSwapResponse{}, nil
|
// If needed, add additional validation based on actual DID structure
|
||||||
|
|
||||||
|
// 2. Validate connection exists and is open
|
||||||
|
if err := ms.ValidateConnection(sdkCtx, msg.ConnectionId); err != nil {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrInvalidConnection, "connection validation failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get or ensure ICA account exists
|
||||||
|
account, err := ms.GetDEXAccount(sdkCtx, msg.Did, msg.ConnectionId)
|
||||||
|
if err != nil {
|
||||||
|
// Account doesn't exist, return error - user must register first
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrAccountNotFound, "DEX account not found for DID %s on connection %s. Please register first.", msg.Did, msg.ConnectionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify account is active
|
||||||
|
if account.Status != types.ACCOUNT_STATUS_ACTIVE {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrAccountNotActive, "DEX account is not active (status: %s)", account.Status.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validate swap parameters
|
||||||
|
tokenIn := sdk.NewCoin(msg.SourceDenom, msg.Amount)
|
||||||
|
if err := ms.ValidateSwapParameters(tokenIn, msg.TargetDenom, msg.MinAmountOut); err != nil {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrInvalidSwapParams, "swap parameter validation failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Build swap message for the target chain
|
||||||
|
// For Noble USDC swaps, we use IBC transfer
|
||||||
|
// For other DEX chains (like Osmosis), we build chain-specific swap messages
|
||||||
|
var swapMsgs []sdk.Msg
|
||||||
|
var swapType string
|
||||||
|
|
||||||
|
// Check if this is a Noble USDC swap
|
||||||
|
if types.IsNobleChain(account.HostChainId) || msg.SourceDenom == types.NobleUSDCDenom || msg.TargetDenom == types.NobleUSDCDenom {
|
||||||
|
// Build Noble-specific swap message (IBC transfer for USDC)
|
||||||
|
swapMsg, err := ms.BuildNobleSwapMsg(sdkCtx, account.AccountAddress, tokenIn, msg.TargetDenom, msg.MinAmountOut)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrSwapFailed, "failed to build Noble swap message: %v", err)
|
||||||
|
}
|
||||||
|
swapMsgs = []sdk.Msg{swapMsg}
|
||||||
|
swapType = "noble_usdc_swap"
|
||||||
|
} else {
|
||||||
|
// Build generic DEX swap message (e.g., Osmosis)
|
||||||
|
swapMsg := ms.BuildOsmosisSwapMsg(account.AccountAddress, 1, tokenIn, msg.TargetDenom, msg.MinAmountOut)
|
||||||
|
swapMsgs = []sdk.Msg{swapMsg}
|
||||||
|
swapType = "osmosis_swap"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Calculate timeout from message or use default
|
||||||
|
timeoutDuration := msg.Timeout.Sub(sdkCtx.BlockTime())
|
||||||
|
if timeoutDuration <= 0 {
|
||||||
|
timeoutDuration = 30 * time.Second // Default 30 second timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Send the swap transaction via ICA
|
||||||
|
sequence, err := ms.SendDEXTransaction(
|
||||||
|
sdkCtx,
|
||||||
|
msg.Did,
|
||||||
|
msg.ConnectionId,
|
||||||
|
swapMsgs,
|
||||||
|
fmt.Sprintf("swap_%s_%s_for_%s", swapType, msg.SourceDenom, msg.TargetDenom),
|
||||||
|
timeoutDuration,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrSwapFailed, "failed to send swap transaction via ICA: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Track transaction in DWN if available
|
||||||
|
if ms.dwnKeeper != nil {
|
||||||
|
// Create swap activity record
|
||||||
|
activity := types.DEXActivity{
|
||||||
|
Type: "swap",
|
||||||
|
Did: msg.Did,
|
||||||
|
ConnectionId: msg.ConnectionId,
|
||||||
|
BlockHeight: sdkCtx.BlockHeight(),
|
||||||
|
Timestamp: sdkCtx.BlockTime(),
|
||||||
|
Status: "pending",
|
||||||
|
Amount: sdk.NewCoins(tokenIn),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in DWN for user history (non-blocking)
|
||||||
|
if err := ms.storeActivityInDWN(sdkCtx, msg.Did, &activity); err != nil {
|
||||||
|
// Log but don't fail the transaction
|
||||||
|
ms.Logger(sdkCtx).Error("failed to store swap activity in DWN", "error", err, "did", msg.Did)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Emit swap event for indexing
|
||||||
|
sdkCtx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeSwapExecuted,
|
||||||
|
sdk.NewAttribute("did", msg.Did),
|
||||||
|
sdk.NewAttribute("connection_id", msg.ConnectionId),
|
||||||
|
sdk.NewAttribute("source_denom", msg.SourceDenom),
|
||||||
|
sdk.NewAttribute("target_denom", msg.TargetDenom),
|
||||||
|
sdk.NewAttribute("amount", msg.Amount.String()),
|
||||||
|
sdk.NewAttribute("min_amount_out", msg.MinAmountOut.String()),
|
||||||
|
sdk.NewAttribute("sequence", fmt.Sprintf("%d", sequence)),
|
||||||
|
sdk.NewAttribute("swap_type", swapType),
|
||||||
|
sdk.NewAttribute("ica_address", account.AccountAddress),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &types.MsgExecuteSwapResponse{
|
||||||
|
TxHash: "", // Will be populated by ICA callback
|
||||||
|
AmountReceived: "", // Will be populated by ICA callback
|
||||||
|
Sequence: sequence,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateUCANPermission validates UCAN token for a DEX operation
|
// validateUCANPermission validates UCAN token for a DEX operation
|
||||||
@@ -200,7 +301,7 @@ func (ms msgServer) CreateLimitOrder(
|
|||||||
// 5. Check order status is still open (not filled or already cancelled)
|
// 5. Check order status is still open (not filled or already cancelled)
|
||||||
// 6. Build order cancellation message for target chain's order book protocol
|
// 6. Build order cancellation message for target chain's order book protocol
|
||||||
// 7. Create ICA packet data with the cancellation transaction
|
// 7. Create ICA packet data with the cancellation transaction
|
||||||
// 8. Send ICA packet through IBC channel and await acknowledgment
|
// 8. Send IBC packet through IBC channel and await acknowledgment
|
||||||
// 9. Update order status in local state to cancelled
|
// 9. Update order status in local state to cancelled
|
||||||
// 10. Update order record in DWN with cancellation details
|
// 10. Update order record in DWN with cancellation details
|
||||||
// Returns: Sequence number on successful cancellation
|
// Returns: Sequence number on successful cancellation
|
||||||
@@ -217,3 +318,26 @@ func (ms msgServer) CancelOrder(
|
|||||||
// 5. Update order status in DWN
|
// 5. Update order status in DWN
|
||||||
return &types.MsgCancelOrderResponse{}, nil
|
return &types.MsgCancelOrderResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// storeActivityInDWN stores a DEX activity record in the DWN module
|
||||||
|
func (ms msgServer) storeActivityInDWN(ctx sdk.Context, did string, activity *types.DEXActivity) error {
|
||||||
|
// For now, this is a stub - actual implementation would interface with DWN keeper
|
||||||
|
// to store activity records in the user's decentralized web node
|
||||||
|
if ms.dwnKeeper == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement actual DWN storage logic
|
||||||
|
// This would involve:
|
||||||
|
// 1. Serializing the activity to JSON
|
||||||
|
// 2. Creating a DWN record with proper schema
|
||||||
|
// 3. Storing it in the user's vault
|
||||||
|
|
||||||
|
ms.Logger(ctx).Info("DEX activity tracked",
|
||||||
|
"did", did,
|
||||||
|
"type", activity.Type,
|
||||||
|
"status", activity.Status,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,3 +123,118 @@ func (k Keeper) ValidateSwapParameters(
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildNobleSwapMsg builds a Noble-specific swap message using IBC transfer
|
||||||
|
// Noble swaps typically involve transferring USDC between chains via IBC
|
||||||
|
func (k Keeper) BuildNobleSwapMsg(
|
||||||
|
ctx sdk.Context,
|
||||||
|
senderAddress string,
|
||||||
|
tokenIn sdk.Coin,
|
||||||
|
tokenOutDenom string,
|
||||||
|
minAmountOut math.Int,
|
||||||
|
) (sdk.Msg, error) {
|
||||||
|
// Validate Noble swap parameters
|
||||||
|
params := types.NobleSwapParams{
|
||||||
|
InputDenom: tokenIn.Denom,
|
||||||
|
OutputDenom: tokenOutDenom,
|
||||||
|
Amount: tokenIn.Amount,
|
||||||
|
MinOutput: minAmountOut,
|
||||||
|
Receiver: senderAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := params.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid Noble swap params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Noble USDC, we primarily use IBC transfers
|
||||||
|
// In a full implementation, this would:
|
||||||
|
// 1. Determine the IBC channel to Noble
|
||||||
|
// 2. Build an IBC transfer message to send USDC
|
||||||
|
// 3. Include proper timeout and memo for swap routing
|
||||||
|
|
||||||
|
// For now, return a placeholder bank send message
|
||||||
|
// In production, this should be an IBC transfer message:
|
||||||
|
// transferMsg := &transfertypes.MsgTransfer{
|
||||||
|
// SourcePort: "transfer",
|
||||||
|
// SourceChannel: channelID,
|
||||||
|
// Token: tokenIn,
|
||||||
|
// Sender: senderAddress,
|
||||||
|
// Receiver: senderAddress,
|
||||||
|
// TimeoutHeight: clienttypes.NewHeight(0, 0),
|
||||||
|
// TimeoutTimestamp: uint64(ctx.BlockTime().Add(30 * time.Second).UnixNano()),
|
||||||
|
// Memo: fmt.Sprintf("swap:%s:%s", tokenOutDenom, minAmountOut.String()),
|
||||||
|
// }
|
||||||
|
|
||||||
|
return &banktypes.MsgSend{
|
||||||
|
FromAddress: senderAddress,
|
||||||
|
ToAddress: senderAddress,
|
||||||
|
Amount: sdk.NewCoins(tokenIn),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildSwapRoute determines the optimal swap route, potentially using USDC as intermediary
|
||||||
|
func (k Keeper) BuildSwapRoute(
|
||||||
|
ctx sdk.Context,
|
||||||
|
tokenInDenom string,
|
||||||
|
tokenOutDenom string,
|
||||||
|
connectionID string,
|
||||||
|
) ([]types.TradingPair, error) {
|
||||||
|
// Simple routing logic:
|
||||||
|
// 1. If either token is USDC, direct swap
|
||||||
|
// 2. Otherwise, route through USDC as intermediary
|
||||||
|
|
||||||
|
if tokenInDenom == types.NobleUSDCDenom || tokenOutDenom == types.NobleUSDCDenom {
|
||||||
|
// Direct swap
|
||||||
|
return []types.TradingPair{
|
||||||
|
{
|
||||||
|
Base: tokenInDenom,
|
||||||
|
Quote: tokenOutDenom,
|
||||||
|
Description: fmt.Sprintf("%s/%s direct", tokenInDenom, tokenOutDenom),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route through USDC
|
||||||
|
return []types.TradingPair{
|
||||||
|
{
|
||||||
|
Base: tokenInDenom,
|
||||||
|
Quote: types.NobleUSDCDenom,
|
||||||
|
Description: fmt.Sprintf("%s/USDC", tokenInDenom),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Base: types.NobleUSDCDenom,
|
||||||
|
Quote: tokenOutDenom,
|
||||||
|
Description: fmt.Sprintf("USDC/%s", tokenOutDenom),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateNobleSwapOutput estimates output for a Noble USDC swap
|
||||||
|
func (k Keeper) EstimateNobleSwapOutput(
|
||||||
|
ctx sdk.Context,
|
||||||
|
tokenIn sdk.Coin,
|
||||||
|
tokenOutDenom string,
|
||||||
|
) (math.Int, error) {
|
||||||
|
// In a full implementation, this would:
|
||||||
|
// 1. Query Noble chain for current exchange rates
|
||||||
|
// 2. Query any DEX pools for pricing
|
||||||
|
// 3. Calculate expected output accounting for fees
|
||||||
|
|
||||||
|
// For now, use a simple 1% fee model
|
||||||
|
estimatedOutput := tokenIn.Amount.MulRaw(99).QuoRaw(100)
|
||||||
|
|
||||||
|
return estimatedOutput, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateSwapSlippage calculates the slippage percentage for a swap
|
||||||
|
func (k Keeper) CalculateSwapSlippage(
|
||||||
|
expectedOutput math.Int,
|
||||||
|
minOutput math.Int,
|
||||||
|
) math.LegacyDec {
|
||||||
|
if expectedOutput.IsZero() {
|
||||||
|
return math.LegacyZeroDec()
|
||||||
|
}
|
||||||
|
|
||||||
|
slippage := math.LegacyNewDecFromInt(expectedOutput.Sub(minOutput)).Quo(math.LegacyNewDecFromInt(expectedOutput))
|
||||||
|
return slippage.Mul(math.LegacyNewDec(100)) // Convert to percentage
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,4 +14,8 @@ var (
|
|||||||
ErrInvalidLiquidityParams = sdkerrors.Register(ModuleName, 9, "invalid liquidity parameters")
|
ErrInvalidLiquidityParams = sdkerrors.Register(ModuleName, 9, "invalid liquidity parameters")
|
||||||
ErrInvalidOrderParams = sdkerrors.Register(ModuleName, 10, "invalid order parameters")
|
ErrInvalidOrderParams = sdkerrors.Register(ModuleName, 10, "invalid order parameters")
|
||||||
ErrICAOperationFailed = sdkerrors.Register(ModuleName, 11, "ICA operation failed")
|
ErrICAOperationFailed = sdkerrors.Register(ModuleName, 11, "ICA operation failed")
|
||||||
|
ErrInvalidConnection = sdkerrors.Register(ModuleName, 12, "invalid IBC connection")
|
||||||
|
ErrSwapFailed = sdkerrors.Register(ModuleName, 13, "swap operation failed")
|
||||||
|
ErrLiquidityFailed = sdkerrors.Register(ModuleName, 14, "liquidity operation failed")
|
||||||
|
ErrOrderFailed = sdkerrors.Register(ModuleName, 15, "order operation failed")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
|
|||||||
// DefaultGenesisState returns the default module GenesisState.
|
// DefaultGenesisState returns the default module GenesisState.
|
||||||
func DefaultGenesisState() *GenesisState {
|
func DefaultGenesisState() *GenesisState {
|
||||||
return &GenesisState{
|
return &GenesisState{
|
||||||
|
Params: DefaultParams(),
|
||||||
PortId: PortID,
|
PortId: PortID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +13,7 @@ func DefaultGenesisState() *GenesisState {
|
|||||||
// NewGenesisState initializes and returns a new GenesisState.
|
// NewGenesisState initializes and returns a new GenesisState.
|
||||||
func NewGenesisState() *GenesisState {
|
func NewGenesisState() *GenesisState {
|
||||||
return &GenesisState{
|
return &GenesisState{
|
||||||
|
Params: DefaultParams(),
|
||||||
PortId: PortID,
|
PortId: PortID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,5 +24,10 @@ func (gs *GenesisState) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate params
|
||||||
|
if err := gs.Params.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"cosmossdk.io/math"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Noble testnet chain configuration
|
||||||
|
const (
|
||||||
|
// NobleTestnetChainID is the chain ID for Noble testnet (grand-1)
|
||||||
|
NobleTestnetChainID = "noble-grand-1"
|
||||||
|
|
||||||
|
// NobleMainnetChainID is the chain ID for Noble mainnet
|
||||||
|
NobleMainnetChainID = "noble-1"
|
||||||
|
|
||||||
|
// NobleUSDCDenom is the denomination for USDC on Noble
|
||||||
|
NobleUSDCDenom = "uusdc"
|
||||||
|
|
||||||
|
// NobleUSDCDecimals is the number of decimals for USDC (6 decimals like native USDC)
|
||||||
|
NobleUSDCDecimals = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// NobleChainConfig holds configuration for Noble blockchain integration
|
||||||
|
type NobleChainConfig struct {
|
||||||
|
ChainID string
|
||||||
|
RPCEndpoint string
|
||||||
|
GRPCEndpoint string
|
||||||
|
USDCDenom string
|
||||||
|
Decimals uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNobleTestnetConfig returns the configuration for Noble testnet
|
||||||
|
func GetNobleTestnetConfig() NobleChainConfig {
|
||||||
|
return NobleChainConfig{
|
||||||
|
ChainID: NobleTestnetChainID,
|
||||||
|
RPCEndpoint: "https://noble-testnet-rpc.polkachu.com:443",
|
||||||
|
GRPCEndpoint: "noble-testnet-grpc.polkachu.com:21590",
|
||||||
|
USDCDenom: NobleUSDCDenom,
|
||||||
|
Decimals: NobleUSDCDecimals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNobleMainnetConfig returns the configuration for Noble mainnet
|
||||||
|
func GetNobleMainnetConfig() NobleChainConfig {
|
||||||
|
return NobleChainConfig{
|
||||||
|
ChainID: NobleMainnetChainID,
|
||||||
|
RPCEndpoint: "https://noble-rpc.polkachu.com:443",
|
||||||
|
GRPCEndpoint: "noble-grpc.polkachu.com:21590",
|
||||||
|
USDCDenom: NobleUSDCDenom,
|
||||||
|
Decimals: NobleUSDCDecimals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNobleChain checks if a chain ID corresponds to Noble (testnet or mainnet)
|
||||||
|
func IsNobleChain(chainID string) bool {
|
||||||
|
return chainID == NobleTestnetChainID || chainID == NobleMainnetChainID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateNobleConnection validates that a connection ID is properly configured for Noble
|
||||||
|
func ValidateNobleConnection(connectionID string, allowedConnections []string) error {
|
||||||
|
for _, allowed := range allowedConnections {
|
||||||
|
if connectionID == allowed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("connection %s not in allowed connections list", connectionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToUSDC converts an amount from base units to USDC representation
|
||||||
|
// For example: 1000000 base units = 1.000000 USDC
|
||||||
|
func ConvertToUSDC(amount math.Int) math.LegacyDec {
|
||||||
|
// Convert to decimal and divide by 10^6
|
||||||
|
return math.LegacyNewDecFromInt(amount).QuoInt64(1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertFromUSDC converts USDC amount to base units
|
||||||
|
// For example: 1.5 USDC = 1500000 base units
|
||||||
|
func ConvertFromUSDC(usdcAmount math.LegacyDec) math.Int {
|
||||||
|
// Multiply by 10^6 and truncate to integer
|
||||||
|
return usdcAmount.MulInt64(1000000).TruncateInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatUSDCAmount formats a USDC amount for display
|
||||||
|
// For example: 1500000 -> "1.500000 USDC"
|
||||||
|
func FormatUSDCAmount(amount math.Int) string {
|
||||||
|
usdcDec := ConvertToUSDC(amount)
|
||||||
|
return fmt.Sprintf("%s USDC", usdcDec.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUSDCAmount parses a string USDC amount to base units
|
||||||
|
// For example: "1.5" -> 1500000
|
||||||
|
func ParseUSDCAmount(amountStr string) (math.Int, error) {
|
||||||
|
usdcDec, err := math.LegacyNewDecFromStr(amountStr)
|
||||||
|
if err != nil {
|
||||||
|
return math.ZeroInt(), fmt.Errorf("invalid USDC amount: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if usdcDec.IsNegative() {
|
||||||
|
return math.ZeroInt(), fmt.Errorf("USDC amount cannot be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertFromUSDC(usdcDec), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NobleSwapParams defines parameters for a swap on Noble or via Noble USDC
|
||||||
|
type NobleSwapParams struct {
|
||||||
|
// InputDenom is the denomination being swapped from
|
||||||
|
InputDenom string
|
||||||
|
// OutputDenom is the denomination being swapped to
|
||||||
|
OutputDenom string
|
||||||
|
// Amount is the input amount in base units
|
||||||
|
Amount math.Int
|
||||||
|
// MinOutput is the minimum output amount (slippage protection)
|
||||||
|
MinOutput math.Int
|
||||||
|
// Receiver is the address to receive the output tokens
|
||||||
|
Receiver string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation on NobleSwapParams
|
||||||
|
func (p NobleSwapParams) Validate() error {
|
||||||
|
if p.InputDenom == "" {
|
||||||
|
return fmt.Errorf("input denom cannot be empty")
|
||||||
|
}
|
||||||
|
if p.OutputDenom == "" {
|
||||||
|
return fmt.Errorf("output denom cannot be empty")
|
||||||
|
}
|
||||||
|
if p.InputDenom == p.OutputDenom {
|
||||||
|
return fmt.Errorf("input and output denoms must be different")
|
||||||
|
}
|
||||||
|
if !p.Amount.IsPositive() {
|
||||||
|
return fmt.Errorf("amount must be positive")
|
||||||
|
}
|
||||||
|
if !p.MinOutput.IsPositive() {
|
||||||
|
return fmt.Errorf("min output must be positive")
|
||||||
|
}
|
||||||
|
if p.Receiver == "" {
|
||||||
|
return fmt.Errorf("receiver cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate receiver is a valid bech32 address
|
||||||
|
_, err := sdk.AccAddressFromBech32(p.Receiver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid receiver address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NobleLiquidityParams defines parameters for providing liquidity via Noble
|
||||||
|
type NobleLiquidityParams struct {
|
||||||
|
// PoolID is the target pool identifier
|
||||||
|
PoolID string
|
||||||
|
// Token0 is the first token denomination
|
||||||
|
Token0 string
|
||||||
|
// Token1 is the second token denomination
|
||||||
|
Token1 string
|
||||||
|
// Amount0 is the amount of first token
|
||||||
|
Amount0 math.Int
|
||||||
|
// Amount1 is the amount of second token
|
||||||
|
Amount1 math.Int
|
||||||
|
// MinShares is the minimum LP shares to receive
|
||||||
|
MinShares math.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation on NobleLiquidityParams
|
||||||
|
func (p NobleLiquidityParams) Validate() error {
|
||||||
|
if p.PoolID == "" {
|
||||||
|
return fmt.Errorf("pool ID cannot be empty")
|
||||||
|
}
|
||||||
|
if p.Token0 == "" {
|
||||||
|
return fmt.Errorf("token0 cannot be empty")
|
||||||
|
}
|
||||||
|
if p.Token1 == "" {
|
||||||
|
return fmt.Errorf("token1 cannot be empty")
|
||||||
|
}
|
||||||
|
if !p.Amount0.IsPositive() {
|
||||||
|
return fmt.Errorf("amount0 must be positive")
|
||||||
|
}
|
||||||
|
if !p.Amount1.IsPositive() {
|
||||||
|
return fmt.Errorf("amount1 must be positive")
|
||||||
|
}
|
||||||
|
if !p.MinShares.IsPositive() {
|
||||||
|
return fmt.Errorf("min shares must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNobleUSDCPairs returns common trading pairs that include Noble USDC
|
||||||
|
// This is useful for routing swaps through USDC as an intermediary
|
||||||
|
func GetNobleUSDCPairs() []TradingPair {
|
||||||
|
return []TradingPair{
|
||||||
|
{Base: "uatom", Quote: NobleUSDCDenom, Description: "ATOM/USDC"},
|
||||||
|
{Base: "uosmo", Quote: NobleUSDCDenom, Description: "OSMO/USDC"},
|
||||||
|
{Base: "uakt", Quote: NobleUSDCDenom, Description: "AKT/USDC"},
|
||||||
|
{Base: "ujuno", Quote: NobleUSDCDenom, Description: "JUNO/USDC"},
|
||||||
|
{Base: "ustars", Quote: NobleUSDCDenom, Description: "STARS/USDC"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TradingPair represents a trading pair on a DEX
|
||||||
|
type TradingPair struct {
|
||||||
|
Base string
|
||||||
|
Quote string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the trading pair
|
||||||
|
func (tp TradingPair) String() string {
|
||||||
|
if tp.Description != "" {
|
||||||
|
return tp.Description
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", tp.Base, tp.Quote)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse returns the reverse trading pair (swaps base and quote)
|
||||||
|
func (tp TradingPair) Reverse() TradingPair {
|
||||||
|
return TradingPair{
|
||||||
|
Base: tp.Quote,
|
||||||
|
Quote: tp.Base,
|
||||||
|
Description: fmt.Sprintf("%s/%s", tp.Quote, tp.Base),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetNobleTestnetConfig(t *testing.T) {
|
||||||
|
config := GetNobleTestnetConfig()
|
||||||
|
|
||||||
|
require.Equal(t, NobleTestnetChainID, config.ChainID)
|
||||||
|
require.Equal(t, "https://noble-testnet-rpc.polkachu.com:443", config.RPCEndpoint)
|
||||||
|
require.Equal(t, "noble-testnet-grpc.polkachu.com:21590", config.GRPCEndpoint)
|
||||||
|
require.Equal(t, NobleUSDCDenom, config.USDCDenom)
|
||||||
|
require.Equal(t, uint8(6), config.Decimals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNobleMainnetConfig(t *testing.T) {
|
||||||
|
config := GetNobleMainnetConfig()
|
||||||
|
|
||||||
|
require.Equal(t, NobleMainnetChainID, config.ChainID)
|
||||||
|
require.Equal(t, "https://noble-rpc.polkachu.com:443", config.RPCEndpoint)
|
||||||
|
require.Equal(t, "noble-grpc.polkachu.com:21590", config.GRPCEndpoint)
|
||||||
|
require.Equal(t, NobleUSDCDenom, config.USDCDenom)
|
||||||
|
require.Equal(t, uint8(6), config.Decimals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNobleChain(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
chainID string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "noble testnet",
|
||||||
|
chainID: NobleTestnetChainID,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noble mainnet",
|
||||||
|
chainID: NobleMainnetChainID,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "osmosis",
|
||||||
|
chainID: "osmosis-1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cosmos hub",
|
||||||
|
chainID: "cosmoshub-4",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := IsNobleChain(tc.chainID)
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNobleConnection(t *testing.T) {
|
||||||
|
allowedConnections := []string{"noble-grand-1", "osmo-test-5"}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
connectionID string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid noble connection",
|
||||||
|
connectionID: "noble-grand-1",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid osmosis connection",
|
||||||
|
connectionID: "osmo-test-5",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid connection",
|
||||||
|
connectionID: "unknown-chain",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := ValidateNobleConnection(tc.connectionID, allowedConnections)
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertToUSDC(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
amount sdk.Int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
amount: sdk.NewInt(1000000),
|
||||||
|
expected: "1.000000000000000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
amount: sdk.NewInt(1500000),
|
||||||
|
expected: "1.500000000000000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0.1 USDC",
|
||||||
|
amount: sdk.NewInt(100000),
|
||||||
|
expected: "0.100000000000000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0 USDC",
|
||||||
|
amount: sdk.ZeroInt(),
|
||||||
|
expected: "0.000000000000000000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := ConvertToUSDC(tc.amount)
|
||||||
|
require.Equal(t, tc.expected, result.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertFromUSDC(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
usdc sdk.Dec
|
||||||
|
expected int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
usdc: sdk.MustNewDecFromStr("1.0"),
|
||||||
|
expected: 1000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
usdc: sdk.MustNewDecFromStr("1.5"),
|
||||||
|
expected: 1500000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0.1 USDC",
|
||||||
|
usdc: sdk.MustNewDecFromStr("0.1"),
|
||||||
|
expected: 100000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0 USDC",
|
||||||
|
usdc: sdk.ZeroDec(),
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := ConvertFromUSDC(tc.usdc)
|
||||||
|
require.Equal(t, tc.expected, result.Int64())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatUSDCAmount(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
amount sdk.Int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
amount: sdk.NewInt(1000000),
|
||||||
|
expected: "1.000000000000000000 USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
amount: sdk.NewInt(1500000),
|
||||||
|
expected: "1.500000000000000000 USDC",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := FormatUSDCAmount(tc.amount)
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseUSDCAmount(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int64
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1 USDC",
|
||||||
|
input: "1.0",
|
||||||
|
expected: 1000000,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1.5 USDC",
|
||||||
|
input: "1.5",
|
||||||
|
expected: 1500000,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "0 USDC",
|
||||||
|
input: "0",
|
||||||
|
expected: 0,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid format",
|
||||||
|
input: "abc",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative amount",
|
||||||
|
input: "-1.0",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := ParseUSDCAmount(tc.input)
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected, result.Int64())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNobleSwapParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params NobleSwapParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid params",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input denom",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "input denom cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty output denom",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "output denom cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same input and output denom",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uusdc",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "input and output denoms must be different",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero amount",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.ZeroInt(),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "amount must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero min output",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.ZeroInt(),
|
||||||
|
Receiver: "cosmos1abc123def456ghi789jkl012mno345pqr678st",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "min output must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty receiver",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "receiver cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid receiver address",
|
||||||
|
params: NobleSwapParams{
|
||||||
|
InputDenom: "uatom",
|
||||||
|
OutputDenom: "uusdc",
|
||||||
|
Amount: sdk.NewInt(1000000),
|
||||||
|
MinOutput: sdk.NewInt(500000),
|
||||||
|
Receiver: "invalid",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "invalid receiver address",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNobleLiquidityParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params NobleLiquidityParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid params",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty pool ID",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "pool ID cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty token0",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "token0 cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty token1",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "token1 cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero amount0",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.ZeroInt(),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "amount0 must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero amount1",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.ZeroInt(),
|
||||||
|
MinShares: sdk.NewInt(1),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "amount1 must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero min shares",
|
||||||
|
params: NobleLiquidityParams{
|
||||||
|
PoolID: "1",
|
||||||
|
Token0: "uusdc",
|
||||||
|
Token1: "uatom",
|
||||||
|
Amount0: sdk.NewInt(1000000),
|
||||||
|
Amount1: sdk.NewInt(500000),
|
||||||
|
MinShares: sdk.ZeroInt(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "min shares must be positive",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNobleUSDCPairs(t *testing.T) {
|
||||||
|
pairs := GetNobleUSDCPairs()
|
||||||
|
|
||||||
|
require.NotEmpty(t, pairs)
|
||||||
|
require.GreaterOrEqual(t, len(pairs), 5)
|
||||||
|
|
||||||
|
// Check that all pairs have USDC as quote
|
||||||
|
for _, pair := range pairs {
|
||||||
|
require.Equal(t, NobleUSDCDenom, pair.Quote)
|
||||||
|
require.NotEmpty(t, pair.Base)
|
||||||
|
require.NotEmpty(t, pair.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTradingPair_String(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
pair TradingPair
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with description",
|
||||||
|
pair: TradingPair{
|
||||||
|
Base: "uatom",
|
||||||
|
Quote: "uusdc",
|
||||||
|
Description: "ATOM/USDC",
|
||||||
|
},
|
||||||
|
expected: "ATOM/USDC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without description",
|
||||||
|
pair: TradingPair{
|
||||||
|
Base: "uatom",
|
||||||
|
Quote: "uusdc",
|
||||||
|
},
|
||||||
|
expected: "uatom/uusdc",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := tc.pair.String()
|
||||||
|
require.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTradingPair_Reverse(t *testing.T) {
|
||||||
|
pair := TradingPair{
|
||||||
|
Base: "uatom",
|
||||||
|
Quote: "uusdc",
|
||||||
|
Description: "ATOM/USDC",
|
||||||
|
}
|
||||||
|
|
||||||
|
reversed := pair.Reverse()
|
||||||
|
|
||||||
|
require.Equal(t, "uusdc", reversed.Base)
|
||||||
|
require.Equal(t, "uatom", reversed.Quote)
|
||||||
|
require.Equal(t, "uusdc/uatom", reversed.Description)
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"cosmossdk.io/math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultParams returns default module parameters.
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return Params{
|
||||||
|
Enabled: true,
|
||||||
|
MaxAccountsPerDid: 5, // Maximum 5 ICA accounts per DID
|
||||||
|
DefaultTimeoutSeconds: 600, // 10 minutes timeout for ICA operations
|
||||||
|
AllowedConnections: []string{
|
||||||
|
// Noble testnet connection - USDC hub for Cosmos
|
||||||
|
"noble-grand-1",
|
||||||
|
// Osmosis testnet - Primary DEX
|
||||||
|
"osmo-test-5",
|
||||||
|
// Other testnets can be added here
|
||||||
|
},
|
||||||
|
MinSwapAmount: "1000", // Minimum 1000 base units
|
||||||
|
MaxDailyVolume: "1000000000000", // 1M units daily volume cap
|
||||||
|
RateLimits: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10, // Maximum 10 operations per block
|
||||||
|
MaxOpsPerDidPerDay: 100, // Maximum 100 operations per DID per day
|
||||||
|
CooldownBlocks: 5, // 5 block cooldown between operations
|
||||||
|
},
|
||||||
|
Fees: FeeParams{
|
||||||
|
SwapFeeBps: 30, // 0.3% swap fee
|
||||||
|
LiquidityFeeBps: 20, // 0.2% liquidity fee
|
||||||
|
OrderFeeBps: 10, // 0.1% order fee
|
||||||
|
FeeCollector: "", // Empty means use module account
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of module parameters.
|
||||||
|
func (p Params) Validate() error {
|
||||||
|
if p.MaxAccountsPerDid == 0 {
|
||||||
|
return fmt.Errorf("max_accounts_per_did must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MaxAccountsPerDid > 100 {
|
||||||
|
return fmt.Errorf("max_accounts_per_did cannot exceed 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.DefaultTimeoutSeconds == 0 {
|
||||||
|
return fmt.Errorf("default_timeout_seconds must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.DefaultTimeoutSeconds > 3600 {
|
||||||
|
return fmt.Errorf("default_timeout_seconds cannot exceed 3600 (1 hour)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate swap amounts
|
||||||
|
if p.MinSwapAmount != "" {
|
||||||
|
minSwap, ok := math.NewIntFromString(p.MinSwapAmount)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid min_swap_amount: %s", p.MinSwapAmount)
|
||||||
|
}
|
||||||
|
if minSwap.IsNegative() {
|
||||||
|
return fmt.Errorf("min_swap_amount cannot be negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MaxDailyVolume != "" {
|
||||||
|
maxVolume, ok := math.NewIntFromString(p.MaxDailyVolume)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid max_daily_volume: %s", p.MaxDailyVolume)
|
||||||
|
}
|
||||||
|
if maxVolume.IsNegative() {
|
||||||
|
return fmt.Errorf("max_daily_volume cannot be negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rate limits
|
||||||
|
if err := p.RateLimits.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid rate_limits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fees
|
||||||
|
if err := p.Fees.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid fees: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of rate limit parameters.
|
||||||
|
func (r RateLimitParams) Validate() error {
|
||||||
|
if r.MaxOpsPerBlock == 0 {
|
||||||
|
return fmt.Errorf("max_ops_per_block must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MaxOpsPerBlock > 1000 {
|
||||||
|
return fmt.Errorf("max_ops_per_block cannot exceed 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MaxOpsPerDidPerDay == 0 {
|
||||||
|
return fmt.Errorf("max_ops_per_did_per_day must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooldown blocks can be 0 (no cooldown)
|
||||||
|
if r.CooldownBlocks > 1000 {
|
||||||
|
return fmt.Errorf("cooldown_blocks cannot exceed 1000")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of fee parameters.
|
||||||
|
func (f FeeParams) Validate() error {
|
||||||
|
// Basis points validation (1 bps = 0.01%)
|
||||||
|
maxBps := uint32(10000) // 100%
|
||||||
|
|
||||||
|
if f.SwapFeeBps > maxBps {
|
||||||
|
return fmt.Errorf("swap_fee_bps cannot exceed %d (100%%)", maxBps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.LiquidityFeeBps > maxBps {
|
||||||
|
return fmt.Errorf("liquidity_fee_bps cannot exceed %d (100%%)", maxBps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.OrderFeeBps > maxBps {
|
||||||
|
return fmt.Errorf("order_fee_bps cannot exceed %d (100%%)", maxBps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fee collector address validation is done by the SDK when set
|
||||||
|
// Empty string means use module account
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultParams(t *testing.T) {
|
||||||
|
params := DefaultParams()
|
||||||
|
|
||||||
|
// Verify basic fields
|
||||||
|
require.True(t, params.Enabled)
|
||||||
|
require.Equal(t, uint32(5), params.MaxAccountsPerDid)
|
||||||
|
require.Equal(t, uint64(600), params.DefaultTimeoutSeconds)
|
||||||
|
|
||||||
|
// Verify Noble testnet is in allowed connections
|
||||||
|
require.Contains(t, params.AllowedConnections, "noble-grand-1")
|
||||||
|
|
||||||
|
// Verify amounts
|
||||||
|
require.Equal(t, "1000", params.MinSwapAmount)
|
||||||
|
require.Equal(t, "1000000000000", params.MaxDailyVolume)
|
||||||
|
|
||||||
|
// Verify rate limits
|
||||||
|
require.Equal(t, uint32(10), params.RateLimits.MaxOpsPerBlock)
|
||||||
|
require.Equal(t, uint32(100), params.RateLimits.MaxOpsPerDidPerDay)
|
||||||
|
require.Equal(t, uint32(5), params.RateLimits.CooldownBlocks)
|
||||||
|
|
||||||
|
// Verify fees
|
||||||
|
require.Equal(t, uint32(30), params.Fees.SwapFeeBps)
|
||||||
|
require.Equal(t, uint32(20), params.Fees.LiquidityFeeBps)
|
||||||
|
require.Equal(t, uint32(10), params.Fees.OrderFeeBps)
|
||||||
|
|
||||||
|
// Validate params
|
||||||
|
err := params.Validate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params Params
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid default params",
|
||||||
|
params: DefaultParams(),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero max accounts",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 0,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_accounts_per_did must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - max accounts too high",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 101,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_accounts_per_did cannot exceed 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero timeout",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 0,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "default_timeout_seconds must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - timeout too high",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 3601,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "default_timeout_seconds cannot exceed 3600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - negative min swap amount",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "-1000",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "min_swap_amount cannot be negative",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - invalid min swap amount format",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "invalid",
|
||||||
|
MaxDailyVolume: "1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "invalid min_swap_amount",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - negative max daily volume",
|
||||||
|
params: Params{
|
||||||
|
MaxAccountsPerDid: 5,
|
||||||
|
DefaultTimeoutSeconds: 600,
|
||||||
|
MinSwapAmount: "1000",
|
||||||
|
MaxDailyVolume: "-1000000",
|
||||||
|
RateLimits: DefaultParams().RateLimits,
|
||||||
|
Fees: DefaultParams().Fees,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_daily_volume cannot be negative",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRateLimitParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params RateLimitParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid default",
|
||||||
|
params: DefaultParams().RateLimits,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero max ops per block",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 0,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 5,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_ops_per_block must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - max ops per block too high",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 1001,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 5,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_ops_per_block cannot exceed 1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - zero max ops per did per day",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10,
|
||||||
|
MaxOpsPerDidPerDay: 0,
|
||||||
|
CooldownBlocks: 5,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "max_ops_per_did_per_day must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid - zero cooldown blocks",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 0,
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - cooldown blocks too high",
|
||||||
|
params: RateLimitParams{
|
||||||
|
MaxOpsPerBlock: 10,
|
||||||
|
MaxOpsPerDidPerDay: 100,
|
||||||
|
CooldownBlocks: 1001,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "cooldown_blocks cannot exceed 1000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeeParams_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
params FeeParams
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid default",
|
||||||
|
params: DefaultParams().Fees,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid - zero fees",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 0,
|
||||||
|
LiquidityFeeBps: 0,
|
||||||
|
OrderFeeBps: 0,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid - max fees",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 10000,
|
||||||
|
LiquidityFeeBps: 10000,
|
||||||
|
OrderFeeBps: 10000,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - swap fee too high",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 10001,
|
||||||
|
LiquidityFeeBps: 20,
|
||||||
|
OrderFeeBps: 10,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "swap_fee_bps cannot exceed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - liquidity fee too high",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 30,
|
||||||
|
LiquidityFeeBps: 10001,
|
||||||
|
OrderFeeBps: 10,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "liquidity_fee_bps cannot exceed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - order fee too high",
|
||||||
|
params: FeeParams{
|
||||||
|
SwapFeeBps: 30,
|
||||||
|
LiquidityFeeBps: 20,
|
||||||
|
OrderFeeBps: 10001,
|
||||||
|
FeeCollector: "",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "order_fee_bps cannot exceed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.params.Validate()
|
||||||
|
if tc.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tc.errMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user